响应式原理探究(对象)
对象的响应式和基本数据类型的响应式的思想是差不多一致的:依赖收集 和 数据发生时改变依赖触发
不同之处在于:对象数据获取的拦截方式不同。
vue2 基于 Object.defineProperty 而 vue3 基于ES6的新数据类型:Proxy。而这两者的区别主要在于:defineProperty需要针对对象的每一个key来进行一个拦截,也就是说如果一个对象有100个key,那么defineProperty就要执行100次,当数据量庞大的时候,是一个不小的开销,而 Proxy,一个对象仅需要代理一次就可以拦截所有的 key。
最简单的响应式原理模型(对象)
主要思路和基本数据类型是一样的,区别在于针对一个对象,我们要返回一个代理对象来实现响应式,这里就用Proxy来进行举例:
// 这里我们定义了一个currentEffect来作为当前的依赖函数,初始为null
let currentEffect = null;
// 这个effectWatch是用来添加依赖函数,并且首次调用依赖函数
function effectWatch(effect) {
currentEffect = effect;
effect();
currentEffect = null;
}
// 这个Dep类就是针对对象中的每个key的依赖管理(容器+收集+触发)
class Dep {
constructor() {
this.effects = new Set();
}
depend() {
currentEffect && this.effects.add(currentEffect);
}
notify() {
this.effects.forEach((effect) => effect());
}
}
// 这里我们定义一个全局的globalMap用来存储对象和依赖的映射
// 这个数据类型用ts来表示是:Map< Object, Map<string, Function[]> >
const globalDepMap = new Map();
// 这里我们写了一个获取一个对象中key的依赖管理对象(Dep对象)
function getDep(target, key) {
let targetDep = globalDepMap.get(target);
if (!targetDep) {
targetDep = new Map();
globalDepMap.set(target, targetDep);
}
let keyDep = targetDep.get(key);
if (!keyDep) {
keyDep = new Dep();
targetDep.set(key, keyDep);
}
return keyDep;
}
// 这里就是响应式对象的本质,针对一个obj返回一个proxy
function reactive(obj) {
return new Proxy(obj, {
// get trap,对get做一个拦截
get(target, key) {
let dep = getDep(target, key);
dep.depend();
return Reflect.get(target, key); // 必须要有return 否则获取不到值
},
// set trap,对set做一个拦截
set(target, key, newValue) {
const res = Reflect.set(target, key, newValue);
let dep = getDep(target, key);
dep.notify();
return res;
},
});
}
这就是一个最简单的对象响应式模型了。来通过一个例子详细说明一下发生了什么。
const person = reactive({
name: "garfield",
age: 20,
school: {
primary: "hust",
university: "whu"
}
});
let bornYear;
effectWatch(() => {
bornYear = 2022 - person.age;
console.log(bornYear);
});
person.age = 21;
先来看这个,我们定义了一个bornYear,这个变量和person里面的age有依赖关系。
那么我们调用effectWatch(传入的参数为一个箭头函数,记作arrowFn)发生的事情如下:
-
设置currentEffect为arrowFn
-
调用这个arrowFn
-
设置bornYear为 2022 - person.age
- 这里我们需要访问person.age,因此会进入get trap
- 获取person对象中age的依赖管理对象(Dep对象)
- 调用Dep.depend来收集依赖
- 返回person.age的值
-
打印bornYear
-
arrowFn调用完毕
-
-
设置currentEffect为null
因此我们可以看到首次打印出的的是2002(是2022 - 20得来的结果)
接着我们对person.age重新赋值,其中发生的事情如下:
-
执行person.age = 21,会进入Proxy的set trap
-
调用Reflect.set将person.age设置为新值并将结果保存至res
-
获取person对象中age的依赖管理对象(Dep对象)
-
调用Dep.notify来触发依赖
- 触发依赖的时候person.age已经被设置为最新的值了
- 并且要注意的是触发依赖的时候依旧会进入到get trap中,但是此时currentEffect是空,因此并不会收集依赖
-
返回res
-
set trap执行完毕
-
这个响应式模型还有很多待改善的地方,比如对象嵌套对象就不能实现响应式。
let school;
effectWatch(()=>{
school = person.school.university
console.log('----reactive----');
console.log(school);
})
// 不具有响应式
person.school.university = "fdu"
// 解决方法
person.school = {
primary: "hust",
university: "sjtu"
}
\