vue3响应式 通过测试用例出发, 明白我们要做什么,再着手写代码。
let work = { time: 1 };
console.log(work.time); // 1
// 1. 现在想通过一个变量来存储我距离下班还剩下几个小时
let dummy = `距离下班还剩下${8 - work.time}小时`;
console.log(dummy); // 距离下班还剩下7小时
// 2. 又工作了一个小时,我们想要 上一个变量 再告诉我们还剩下几个小时下班
work.time++;
dummy = `距离下班还剩下${8 - work.time}小时`; // 3. 需要再次执行语句才能更新dummy的数值
现在可以明确第一个测试需求 : 想要变量 dummy 随着另一个变量 work.time 来改变
结合上面的代码情境也就是 ,
当
work.time改变时候 , dummy =距离下班还剩下${8 - work.time}小时, 这句话要再被执行。
我们把要执行的函数包裹在 effect函数中,每次修改work.time就执行effect函数 , 也就引出了 vue3 中的effect函数
effect
// 1. 最简单地来做 , 每次work.time 一改变 , 手动去触发 effect函数就可以实现上面的需求了
let work = { time: 1 };
let dummy;
function effect() {
dummy = `距离下班还剩下${8 - work.time}小时`;
console.log(dummy);
}
work.time++;
effect(); // 距离下班还剩下6小时
进一步 , 要考虑的是如何能够在work.time 改变的时候, 自动地触发 effect 函数 , 替代手动地触发
引入proxy代理来监听 work 对象源的数据变化与访问 ( mdn proxy 教程) , 通过 reactive 包裹 work 对象 , 来监听对该对象的操作。
reactive
function reactive(target) {
return new Proxy(target, {
get(target, key) {
console.log('我被访问了')
return Reflect.get(target, key);
},
set(target, key, value) {
// 1. 我要想办法触发对应的 effect 函数
console.log('我被改变了,我要做点什么')
return true;
},
});
}
const work = reactive( {time : 1} )
work.time++
每次修改 work 对象的属性 , 我们都会访问 proxy 的 set方法 , 就可以在这里去触发effect函数
但是我们只需要 改变work.time 时候,才触发我们要的函数 ,而set函数会在修改任意属性时候都触发,那么如何把effect函数和 work.time(或者其他属性) 进行相应的绑定 , 变成我们要完成的需求。
targetMap
在第一次执行effect函数时候,一定会访问到 proxy 的get方法(dummy = work.time 访问了get方法), 所以可以在get中先存储对应的effect函数, 通过以下的数据结构来绑定 work -> time -> effect
targetMap -- (work对象) --> depMap -- (work对象的time属性) --> dep
如上的存储关系可以通过 work.time 在 targetMap-->depMap -->dep中拿到和 work.time 相关的所有effect函数
那么逻辑就变成了 第一次定义effect函数时候 , 要去触发 proxy的get方法,通过对象属性的区分 把 effect函数放到对应的数据结构中, 在下次该对象的属性修改时候,触发set方法, 再通过对象属性的区分从数据结构中把effect函数拿出来执行, 也就完成了 effect 函数随着变量的改变而重新被执行了
const targetMap = new Map();
export function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
// 通过 target 和 key 拿到了存储对应effect的 dep集合
// dep (Set) : [()=>{dummy = work.time +1;} , ()=>{dummy = work.time *2 ;} ...]
}
整理集合起来,要实现的代码功能与拆分如下
全流程
// 1. 定义响应式对象work , 每次访问或者修改work的属性都会触发 get / set
let work = reactive({time: 1});
let dummy;
// 2. 把fn传入effect函数 , 跳转到3
effect(()=>{
// 4. 访问work.time 访问get方法,跳转5
// 8. 访问get结束 , 拿到work.time的值 , 同时执行完fn,跳转9
dummy = `距离下班还剩下${8 - work.time}小时`;
console.log(dummy);
})
// 10. (work.time = work.time+1)
// 改变work.time ,先访问get ,在track中发现affectEffect为null,跳出 , 继续访问set , 跳转11
work.time++; // 13. fn重新被执行,输出 距离下班还剩下6小时
# effect.ts
let activeEffect;
function effect(fn) {
// 3. 把fn保存到全局变量activeEffect, 然后执行 fn() , 跳转4
activeEffect = fn;
fn();
// 9. fn执行结束,把全局activeEffect清空(防止effect函数外访问track),effect执行结束 ,跳转10
activeEffect = null;
}
const targetMap = new Map();
function track(target, key) {
if (!activeEffect) return;
// 6. 通过target和key以及全局的activeEffect,把响应函数fn放入到对应的数据结构中,跳转7
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
}
function trigger(target, key, value) {
// 12. 通过target和key,从数据结构中拿到所有之前存过的dep依赖,执行他们(中间会再触发get,同样通过activeEffect拦截不进入track),跳转13
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) return;
for (const fn of dep) {
fn();
}
}
# reactive.ts
function reactive(target) {
return new Proxy(target, {
get(target, key) {
// 5. 触发track函数,传入 响应式对象和响应式对象target的属性key ,跳转6
track(target, key);
// 7. 返回 target[key]的值 ,跳转8
return Reflect.get(target, key);
},
set(target, key, value) {
Reflect.set(target, key, value);
// 11. 触发trigger函数,传入 响应式对象和响应式对象target的属性key ,跳转12
trigger(target, key, value);
return true;
},
});
}
总结
- 定义reactive响应式对象,添加get \ set 劫持
- 用effect函数包裹要被重复执行的函数,默认执行一次
- 触发get同时把该重复执行的函数放入对应的依赖中
- 当响应源改变时,遍历执行对应依赖中的函数
emm这是我第一次写文章,发现写文章的思路和写代码的逻辑还是有许多区别的。想要用文字把代码的执行顺序表达清楚比我想得还是难些(打断点看思路会更清晰些),没啥经验,再接再厉吧。仓库地址 mini-vue