前言
Vue 最独特的特性之一,是其非侵入性的响应式系统,这篇文章将由浅入深的一步步探索一下 Vue 的响应式系统到底是如何实现的。
思考
假如有一个数据状态,被其他模块(例如一个函数)所引用,当数据状态发生了改变,我们也希望引入该数据的模块里面也能得到响应,根据数据状态的变化进行重新计算,例如下面这个例子:
cosnt data = {
counter: 100
}
function double() {
const res = data.counter * 2
console.log(res)
}
// 数据发生变化,重新执行函数
data.counter++
double()
上面的例子中,double函数里面使用了data.counter这个数据,当data.counter发生变化之后,我们想要double函数重新计算,那我们就必须要重新执行一遍double函数。
但是这样有个很大的问题,如果有很多的模块都应用了这个数据,那么当数据发生变更,所有的模块都要做更新操作,如上的例子就是,所有的函数都需要重新执行一遍。
那有什么办法能够更高效的去完成这个操作呢?
优化一:统一通知更新
我们希望当有很多模块依赖于某个数据状态,当该数据发生变化的时候,我们只需要做一次通知操作,所有的模块就都能自动的完成更新操作,代码如下:
class Dep {
watchers: Set<Function>;
constructor() {
this.watchers = new Set();
}
addEffect(effect: Function) {
this.watchers.add(effect);
}
notify() {
this.watchers.forEach((effect) => {
effect && effect();
});
}
}
const dep = new Dep();
const data = {
counter: 100,
};
const double = () => {
const double = data.counter * 2;
console.log(double);
};
const plus10 = () => {
const plus10 = data.counter + 10;
console.log(plus10);
};
dep.addEffect(double);
dep.addEffect(plus10);
data.counter++;
dep.notify();
如上,double和plus10两个副作用函数都依赖data.counter数据,当data.counter发生变化的时候,调用dep.notify()就可以进行统一通知更新了。
以上代码虽然实现了统一更新,但是多个模块引用数据,我们仍然需要一个一个的将这些模块手动的添加到副作用列表,我们继续优化。
优化二:自动收集依赖
let activeEffect: Function = null
const watchEffect = (effect: Function) => {
activeEffect = effect
dep.depend()
effect()
activeEffect = null
}
在Dep类中新增depend方法添加副作用函数
...
depend(activeEffect) {
this.addEffect(activeEffect)
}
...
这样我们就不用再去手动的一个个的添加副作用函数,而是将副作用函数通过watchEffect函数进行包裹,完整代码如下:
class Dep {
watchers: Set<Function>;
constructor() {
this.watchers = new Set();
}
addEffect(effect: Function) {
this.watchers.add(effect);
}
depend() {
this.addEffect(activeEffect);
}
notify() {
this.watchers.forEach((effect) => {
effect();
});
}
}
let activeEffect: Function = null;
const watchEffect = (effect: Function) => {
// 通过watchEffect包裹的函数赋值给activeEffect
activeEffect = effect;
// 然后将该函数添加到副作用列表
dep.depend();
// 被包裹的函数默认会执行一次
effect();
// 重置
activeEffect = null;
};
const dep = new Dep();
const data = {
counter: 1,
};
watchEffect(function () {
const double = data.counter * 2;
console.log(double);
});
watchEffect(function () {
const plus10 = data.counter + 10;
console.log(plus10);
});
data.counter++;
dep.notify();
到这里,我们再看还有哪些问题,我们设想,当 data 中还有其他属性,比如data.name = 'zhangsan',部分模块依赖于data.counter,部分模块依赖data.name,按照当前已经实现的代码,如果只有data.counter发生了变化,所有的副作用函数都会重新执行。造成这个问题的关键在于,目前只定义了一个dep实例,所有的数据共用同一个实例,当调用dep.notify()的时候,会通知到所有的副作用函数,继续优化。
优化三:依赖项相互独立
先上完整代码(部分关键说明见代码注释):
interface Target {
[key: string]: any;
}
class Dep {
watchers: Set<Function>;
constructor() {
this.watchers = new Set();
}
addEffect(effect: Function) {
this.watchers.add(effect);
}
depend() {
this.addEffect(activeEffect);
}
notify() {
this.watchers.forEach((effect) => {
effect && effect();
});
}
}
let activeEffect: Function = null;
const watchEffect = (effect: Function) => {
activeEffect = effect;
// effect执行,effect函数体代码获取数据,就会触发属性get操作
effect();
activeEffect = null;
};
const targetMap = new WeakMap();
function getDep(target: Object, key: string | symbol): Dep {
// 1 根据对象(target)取出对应的Map对象
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 2 取出对应的dep对象
let dep = depsMap.get(key);
if (!dep) {
dep = new Dep();
depsMap.set(key, dep);
}
return dep;
}
// 对数据进行劫持
function reactive<T extends object>(target: T): T;
function reactive(raw: Target) {
return new Proxy(raw, {
get(target: Target, key: string) {
// 当获取数据的时候就会自动添加依赖
const dep = getDep(target, key);
dep.depend();
return target[key];
},
set(target: Target, key: string, newValue: unknown) {
// 当对数据进行修改的时候,获取数据对应的dep,然后进行针对性的通知
const dep = getDep(target, key);
target[key] = newValue;
dep.notify();
return true;
},
});
}
// 测试
const data1 = reactive({ counter: 1, name: "zhangsan" });
const data2 = reactive({ age: 18 });
// watchEffect1
watchEffect(function () {
console.log("watchEffect1-------", data1.counter * 2, data1.name);
});
// watchEffect2
watchEffect(function () {
console.log("watchEffect2-------", data1.counter * data1.counter);
});
// watchEffect3
watchEffect(function () {
console.log("watchEffect3-------", data1.counter + 1, data1.name);
});
// watchEffect4
watchEffect(function () {
console.log("watchEffect4-------", data2.age);
});
data2.age = 20;
以上代码主要实现了两个函数的封装,getDep函数和reactive函数,对于独立的dep实例的存储,,我们使用WeakMap数据结构,一个原因是WeakMap数据结构的键是一个对象,另一个就是WeakMap数据结构对对象的引用是弱引用,对于内存回收友好。这样我们就能够通过对象及相应的键名方便的找到对应的dep实例。
getDep函数做的事情主要就是根据目标对象及键名,获取对应的dep实例,实例没有创建就创建,已经创建了就直接获取。
reactive函数做的事情主要是对数据进行劫持,获取数据时自动添加依赖,修改数据时自动的进行通知。
到这里就实现了对于数据依赖进行精准的自动收集,当数据发生变化时,自动的进行更新,不过还只是实现了最简单的对象,如果有嵌套的情况,还需要通过递归实现深层次监听。