Vue 面试题:深入解析 watch 的执行机制
本文档围绕一个经典的 watch 面试题展开,层层递进,深入剖析其背后的响应式原理和执行细节。
一、面试原题
请分析以下 Vue 3 代码段,并预测其最终的控制台输出。
javascript
import { reactive, watch } from 'vue';
const state = reactive({ a: 1, b: 2 });
console.log("代码开始执行");
watch(
// 源函数 (Source)
() => {
const sum = state.a + state.b;
console.log(`源函数执行, a+b = ${sum}`);
return sum;
},
// 回调函数 (Callback)
(newValue, oldValue) => {
console.log("回调函数触发!");
}
);
setTimeout(() => {
console.log("1秒后, 修改 state");
state.a++; // a 变为 2
state.b--; // b 变为 1
}, 1000);二、答案与解析
正确答案
代码开始执行
源函数执行, a+b = 3
1秒后, 修改 state
源函数执行, a+b = 3核心结论:源函数执行了两次,回调函数一次也没有执行。
为什么是这个结果?分步解析
初始化阶段
console.log("代码开始执行")输出。watch函数被调用。根据其规则,源函数会立即执行一次,以进行依赖收集并获取初始值。- 控制台输出
源函数执行, a+b = 3。 - 在这次执行中,
watch发现源函数读取了state.a和state.b,于是将它们作为依赖项进行追踪。 watch记录下源函数的初始返回值为3。
setTimeout阶段- 1秒后,
setTimeout的回调开始执行。 console.log("1秒后, 修改 state")输出。state.a++和state.b--被执行。由于state.a和state.b都是被追踪的依赖项,它们的变更会触发watch的响应。- 源函数被再次触发执行。
- 在这次执行中,
state.a是2,state.b是1。a+b的结果仍然是3。 - 控制台输出
源函数执行, a+b = 3。
- 1秒后,
回调触发判断
- 源函数第二次执行后,
watch比较其返回值。 - 新的返回值 (
3) 与上一次的返回值 (3) 完全相等。 - 根据
watch的核心规则,只有当源函数的返回值发生变化时,回调函数才会被触发。 - 因此,回调函数不会执行。
- 源函数第二次执行后,
三、原理深挖:watch 的核心工作法则
从上述题目可以总结出 watch 的三大核心工作法则。
法则一:watch 的两个函数角色
watch 主要由两个函数参数构成:
- 源函数 (Source): 它的职责是:
- 执行并返回一个值:这个值可以是任何类型。
- 触发依赖收集:在它执行期间,所有被读取的响应式数据属性都会被记录为依赖。
- 回调函数 (Callback): 只有在源函数的返回值变化时,它才会被调用,用于执行后续逻辑。
法则二:依赖收集的本质是“读取”
一个响应式数据是否被 watch 追踪,关键在于它是否在源函数的执行过程中被读取 (read) 过。
思考题 1:如果
setTimeout中修改的是一个未被读取的属性state.c,会发生什么? 答案:什么都不会发生。因为state.c从未在源函数中被读取,所以它不是依赖项,它的改变不会触发源函数重新执行。
法则三:回调的触发条件是“返回值变化”
这是 watch 最容易被误解的地方。触发回调的不是依赖项的变化,而是源函数返回值的变化。
思考题 2:如果在源函数中
console.log(state.c),但在setTimeout中改变c,会发生什么?javascript// 源函数 () => { console.log(`c is ${state.c}`); // 读取了 c,c 成为依赖 return state.a + state.b; // 但返回值与 c 无关 } // setTimeout 中 state.c++答案:源函数会重新执行(因为依赖项
c变了),但回调函数依然不会执行(因为返回值a+b的结果没变)。
四、进阶面试题:边界情况探测
题目 1:watch 一个对象本身会怎样?
javascript
const state = reactive({ a: 1 });
watch(
() => state, // 源函数直接返回 state 对象
() => { console.log("State 变化了!"); }
);
setTimeout(() => { state.a++; }, 1000);答案:回调函数不会执行。
解析: watch 默认进行的是浅比较。源函数每次返回的都是 state 这个 Proxy 对象的引用。虽然 state 内部的属性 a 变了,但 state 对象的引用地址本身没有变。因此 watch 认为返回值没有变化。
如何解决:
- 深度监听:使用
deep: true选项。watch(() => state, callback, { deep: true }) - 监听具体属性:返回一个包含你想监听的值的新对象或数组。
() => ({ ...state })
题目 2:依赖收集能穿透函数调用吗?
javascript
const user = reactive({ name: "Tom" });
function getUserName() {
return user.name; // 在另一个函数里读取
}
watch(
() => {
// 源函数调用了另一个函数来返回值
return getUserName();
},
() => { console.log("User name 变了!"); }
);
setTimeout(() => { user.name = "Jerry"; }, 1000);答案:回调函数会执行。
解析: 依赖收集会追踪源函数同步执行的整个调用栈。无论响应式数据是在源函数内部直接读取,还是在它调用的其他函数中被读取,都会被成功收集为依赖。
总结:面试核心要点
- 执行时机:
watch初始化时,源函数立即执行一次。 - 依赖收集:只追踪在源函数执行期间被读取过的响应式数据属性。
- 回调触发:仅当源函数的返回值与上一次相比发生变化时,回调才执行(默认浅比较)。
- 对象监听:直接监听对象引用默认不生效,需使用
deep: true或返回新对象来监听内部变化。