vue 也用了这么多年了,从入门的 2 到现在的 3,每次想表达它的响应式原理总是磕磕绊绊的。 所以我觉得有必要总结精炼一下,下次再被问到的话就能流畅的表达出来的。
事先说明下,以我半吊子的能力可能描述的不是很完善,参考就行。
2 没有必要再说这么多了,直接 vue3 的 setup 响应式是怎么实现的吧。
我们知道在 vue3 里,通过reactive或者是ref我们可以创建响应式的数据。当我们触发 set 时,vue 会自动去更新 ui。
这其中的原理又是什么呢?
开始
当我们通过reactive创建一个响应式数据的时候,它会在内部通过 proxy 劫持 get 和 set 操作,即:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
...
return Reflect.get(target, key)
},
set(target, key, value) {
...
const result = Reflect.set(target, key, value);
return result;
}
})
}
通过上面这个基础的 reactive 函数,我们应该能大概猜得到: 当触发 get 的时候,vue 会在这里收集相关的依赖,记录是谁用了我。 而触发 set 时 vue 会根据 get 里收集的依赖去通知其异步调度批量更新 ui;
下面来模拟 reactive + effect(watchEffect): 声明一个变量用于记录正在运行的 effect 函数;
let activeEffect = null;
声明 effect 函数,vue 的 watchEffect 是传入一个回调函数,在这个回调函数里如果有响应式数据则会在每次数据变化时都会执行这个回调函数。
function effect(cb) {
activeEffect = cb;
// 默认先执行一次并执行完后清空正在执行的effect回调
cb();
activeEffect = null;
}
声明一个 Map 对象用于收集依赖后执行响应的回调事件
const bucket = new WeakMap();
依赖收集
function track(target, key) {
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect); // 记录当前 effect
}
触发通知
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
if (!effects) return;
effects.forEach((fn) => fn()); // 重新执行 effect 函数
}
修改上面的 reactive 函数,在 get 时触发依赖收集,而在 set 时触发通知。
...
get(...) {
if (activeEffect) {
track(target, key); // 收集依赖
}
...
}
set(...) {
...
trigger(target, key); // 触发更新
...
}
...
此时我们使用 reactive 创建一个响应式数据并使用 effect 输出每次变化时的回调
const state = reactive({ count: 0 });
effect(() => {
console.log(`副作用执行:count =`, state.count);
});
state.count++;
state.count++;
此时会输出:
副作用执行:count = 0
副作用执行:count = 1
副作用执行:count = 2
我们来捋一下这期间发生了什么,通过 ractive 创建了一个响应式数据,此时通过 proxy 拦截 get 和 set 方法,并在 get 方法里进行依赖收集,记录到底是谁使用了我,建立依赖关系。 所以每个响应式对象的属性都维护着一组依赖它的副作用函数。 执行 set 时,它会根据 target.key 找到对应的依赖关系,是谁在使用我,然后重新执行这些副作用函数。
在 effect 里我们使用了 state.count,触发了 get 方法,此时 activeEffect 就是当前的回调函数,所以直接进入到 track 函数里来并保存到 WeakMap 里,这个过程就是依赖收集。 而当我们 state.count++即触发了 set 时,触发 trigger 进而根据 targe.key 收集的依赖关系而触发对应的事件。
总结
当我们通过 reactive 创建一个响应式对象后,它内部通过 Proxy 劫持了 get 和 set 操作。
我们用 effect(fn) 注册一个副作用函数时,它会立即执行这个函数,并将 fn 存在 activeEffect 变量中。在这个执行过程中,如果访问了响应式对象的属 ß 性(触发 get),那么这个属性会通过 track() 把当前的 fn 注册为它的依赖。
以后当这个属性发生变化(触发 set),Vue 就会通过 trigger() 找到这个属性的依赖列表,然后执行对应的副作用函数,触发更新逻辑。