观察下面的代码,你猜猜会打印什么?注意,不用太关注 setTimeout 不是 EventLoop 问题。
const count = ref(1);
watchEffect(() => {
setTimeout(() => {
console.log(count.value);
});
});
setTimeout(() => {
count.value = 6;
}, 1000);
以上代码只会打印 1,我们稍微把代码改一下,就可以让它先打印 1,过 1 秒再打印 6 在线示例 (opens new window)。
const count = ref(1);
watchEffect(() => {
+ const value = count.value;
setTimeout(() => {
- console.log(count.value);
+ console.log(value);
});
});
setTimeout(() => {
count.value = 6;
}, 1000);
为什么会这样?我写一个超级简单的 watchEffect 来解析其原理:
/**
* 超级简单 watchEffect Demo
*/
let activeEffect = null;
// 1. 调用 activeEffect 时,将传递过来的回调存储在 activeEffect 上
const watchEffect = (effect) => {
activeEffect = effect;
effect();
// 当一次调用完毕之后,将当前 activeEffect 重置为 null
// 这样是为了确保在多次调用 watchEffect 时,activeEffect 不会因为上一次调用
// 的值使得下一次调用收集依赖时逻辑混乱
// ** 注意 **
// 这里就解释了为什么 watchEffect 在使用异步取值操作之后再次赋值不触发的问题?
// 因为在异步取值调用之前(似乎还是要有点 EventLoop 知识 ^_^),
// activeEffect 已经置空,导致接下来的存储依赖逻辑没有依赖函数要关联,
// 没有关联依赖就导致下次赋值时不会再次触发
activeEffect = null;
};
const ref = (init) => {
// 使用 proxy 在存取值时添加额外的逻辑
const handler = {
get: function(obj, prop) {
// 2.
// 初次调用 watchEffect 时,如果有取值操作且 activeEffect 有值时,
// 我们将 activeEffect存入当前对象的 _dependEffect 依赖数组中,
// 使它们发生关联,以便后续赋值时再次调用
if (prop === "value") {
if (!obj._dependEffect) {
obj._dependEffect = [];
}
if (activeEffect) {
obj._dependEffect.push(activeEffect);
}
}
return obj[prop];
},
set: function(obj, prop, value) {
obj[prop] = value;
// 3. 当发生赋值操作时,遍历对象的 _dependEffect ,实现
// 值发生变化时,再次调用 watchEffect 的效果
if (prop === "value") {
obj._dependEffect.forEach((effect) => {
effect();
});
}
},
};
return new Proxy({ value: init }, handler);
};
const count = ref(1);
watchEffect(() => {
console.log(count.value);
});
setTimeout(() => {
count.value = 6;
}, 1000);
当然,实际上的 watchEffect
实现没有这么简单,还有很多细节没处理,但也没有多复杂,感兴趣的可以直接阅读 effect 的源码 https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/effect.ts (opens new window),注意这不是 watchEffect 的实现,但是 watchEffect 底层是调用 reactivity 包,理解原理看这个源码就行。