
文章目录 5
题目
下面是一个商品筛选组件:
import { ref, watch } from 'vue';
interface Product {
id: number;
name: string;
price: number;
}
const products = ref<Product[]>([
{ id: 1, name: 'Keyboard', price: 300 },
{ id: 2, name: 'Mouse', price: 100 },
]);
const keyword = ref('');
const filteredProducts = ref<Product[]>([...products.value]);
const totalPrice = ref(0);
watch(keyword, () => {
filteredProducts.value = products.value.filter(product =>
product.name.toLowerCase().includes(keyword.value.toLowerCase()),
);
totalPrice.value = filteredProducts.value.reduce(
(total, product) => total + product.price,
0,
);
});
function addProduct(product: Product) {
products.value.push(product);
}请回答:
- 这段代码可能出现哪些数据不一致问题?分别在什么操作下发生?
- 你会如何重构这段代码?请写出核心实现。
- 在这个场景中,你选择的响应式 API 和
watch有什么本质区别?什么情况下才更适合使用watch?
我的回答
- 页面初始化的时候, filteredProducts和totalPrice对不上;
- 写一个页面初始化函数,将初始的商品复制给filteredProducts,并计算totalPrice;也可以采用watchEffect API
- watchEffect只要内部ref或者reactive变量值有变动,就会自动触发;其实watch价格immediate属性也可以实现,watch还是更适合后发的监听
题解
结论:你识别了初始化问题,也了解 watchEffect 和 immediate,但没有抓住核心设计原则:纯派生状态应优先使用 computed,而不是手动同步多个可写状态。
评分:45/100
- 正确性:20/40
- 原理理解:8/20
- 工程应用:8/20
- 边界与权衡:3/10
- 面试表达:6/10
正确点
- 初始化时
totalPrice为0,确实与filteredProducts不一致。 - 知道
watchEffect能自动追踪其同步执行过程中读取的响应式依赖。 - 知道
watch可以通过immediate: true初始化执行。
注意:filteredProducts 初始化时没有问题,因为已经复制了 products.value;不一致的是 totalPrice。
遗漏点
执行 addProduct() 后,keyword 没变,所以原来的 watch(keyword) 不会触发:
addProduct({ id: 3, name: 'Monitor', price: 1000 });此时:
products已更新;filteredProducts仍是旧数据;totalPrice仍是旧价格。
另外,filteredProducts 和 totalPrice 都可以被其他代码直接修改,容易破坏一致性。
更合理的重构
import { computed, ref } from 'vue';
const products = ref<Product[]>([
{ id: 1, name: 'Keyboard', price: 300 },
{ id: 2, name: 'Mouse', price: 100 },
]);
const keyword = ref('');
const filteredProducts = computed(() => {
const normalizedKeyword = keyword.value.toLowerCase();
return products.value.filter(product =>
product.name.toLowerCase().includes(normalizedKeyword),
);
});
const totalPrice = computed(() =>
filteredProducts.value.reduce(
(total, product) => total + product.price,
0,
),
);products 和 keyword 是源状态;另外两个值都可以由源状态计算得出,不需要额外存储。
API 的职责区别
computed:描述一个纯派生值,有缓存,适合本题。watchEffect:自动追踪依赖并执行副作用,不适合代替普通的派生计算。watch:明确监听指定数据,适合请求接口、保存数据、写日志等副作用,也适合需要新旧值或精确控制执行时机的场景。
比“watch 适合后发监听”更准确的面试表达是:
能由已有响应式状态计算出来的值优先使用
computed;需要因状态变化执行外部副作用时,再使用watch或watchEffect。
追问
如果 keyword 改变后需要请求服务端搜索商品,而不是在前端过滤,你会选择 computed、watch 还是 watchEffect?为什么?又该如何处理连续输入造成的请求竞态?
这里只有 keyword 一个明确的监听源,因此 watch 更合适:
let latestRequestId = 0;
watch(keyword, (value, _oldValue, onCleanup) => {
const requestId = ++latestRequestId;
const controller = new AbortController();
const timer = setTimeout(async () => {
try {
const data = await searchProducts(value, controller.signal);
if (requestId === latestRequestId) {
products.value = data;
}
} catch (error) {
if (!(error instanceof DOMException && error.name === 'AbortError')) {
console.error(error);
}
}
}, 300);
onCleanup(() => {
clearTimeout(timer);
controller.abort();
});
});watchEffect 更适合依赖较多、希望自动收集依赖的副作用;异步使用时还要注意,它只追踪第一次 await 之前同步访问的响应式依赖。
面试表达优化
服务端搜索属于由明确状态变化触发的副作用,因此我会使用
watch监听keyword。防抖负责降低请求频率,AbortController负责取消旧请求,递增的requestId负责忽略过期响应,从而保证结果一致性。
区分两组概念:
computed管理纯派生值,watch管理明确依赖产生的副作用。- 防抖优化频率,取消请求节省资源,
requestId保证结果正确性。
相关文章
- 面试题-基础
Vue 3 响应式解构:为什么替换 user 后出现“李四 / 王五”
通过一道 Vue 3 代码题,区分 reactive 对象解构、嵌套属性修改与对象替换的行为,并用 toRef 保持源属性关联。文章保留首次作答及追问补答,说明从误判到纠正的过程。
- 面试题-基础
Vue 3 编辑弹窗:草稿状态、取消重开与父子数据契约
以一个始终挂载的姓名编辑弹窗为场景,给出覆盖数据归属、保存取消、重开初始化和外部更新冲突的完整 Vue 3 解法。结合首次回答与递进补答,说明本地草稿、脏标记和版本校验各自解决的问题。
- 面试题-基础
Vue 3 首屏白屏 3 秒:从性能定位到 Long Task、Web Worker 与虚拟列表
一次完整的 Vue 3 首屏性能面试复盘,从环境复现、Network 与 Performance 定位,逐步深入到 Long Task 分析、Web Worker 和虚拟列表的适用边界。
评论
0 条评论会在审核后展示,邮箱不会公开。