Vue3响应式原理基本概念及手动实现
Posted by Mars . Modified at
Vue3的响应式原理
一、 Vue3响应式基本实现原理
假设reacObj是Vue中的一个响应式对象,它具有属性a和b;
Vue3使用reactive(obj)函数,创建一个响应式的对象,返回一个obj的代理对象Proxy。
reactive内部使用track、effect和trigger三个关键方法来描述响应式过程:
- track(obj, property):对obj中的property进行响应式追踪;
- effect: 依赖某一个响应式property的变量的计算方法函数;
- trigger(obj, property):对依赖obj.property的所有变量计算方法进行重新调用。
基本流程是:
reacitive函数使用Proxy的捕获器,对响应式对象property的get和set操作进行捕获。
- 当get的时候,对属性进行track操作;
- 当set的时候,对属性进行trigger操作。
解释:
当get操作被捕获,说明有变量计算需要访问这个响应式属性,也就是依赖这个属性,所以要对这次get操作的effect进行追踪,添加到dep中以便后续进行响应式更新。
当set操作被捕获,说明有变量被更改,那么需要找到这个变量的dep依赖集,并运行内部的全部方法,来更新依赖它的全部变量。
1.1 对于响应式对象内部属性pbj.prop
对于属性a,如果我们用一个集合Set记录了依赖a的所有变量和它们的计算方法函数(这些计算方法函数叫做effect),在更新a的时候,将这些方法一一重新调用一遍,就可以实现a更改之后依赖它的变量的实时更新,也就是a属性成为了响应式的。
Vue中,为每个响应式对象内部属性所建立的Set对象称为一个dep(PS:dependency,依赖集),只要在修改任何响应式变量后,对应的依赖集dep内方法全部运行一次,就能实现其他变量的更新。
使用Set对象的理由是:它可以保证依赖集内部没有重复。
1.2 对于响应式对象本身obj
一个响应式对象本身可能具有多个属性,需要为它们每个单独建立一个dep依赖集,然后用另一个映射Map将对象属性和dep一一对应。
当track另一个对象属性property2的时候,就把这个property2也添加到映射Map里,然后把所有依赖property2的变量计算方法添加到新的对应dep中,并在Map中与property2对应起来。
这样,这个Map就记录了这个对象obj的全部属性的依赖信息,并可以根据属性更新其对应的那一部分依赖变量。这个Map变量名为depsMap。
1.3 对于整个Vue应用实例中所有响应式对象
一个Vue应用实例,可能声明有多个响应式对象,每一个响应式对象及其内部属性都应该被记录并追踪(track)。
整个应用实例使用一个WeakMap,记录应用实例中的每个响应式对象。这个WeakMap为targetMap。
1.4 effect(fn)的实现
- 假设一个fn在执行的过程中,引用了某一响应式对象obj的属性prop;
- 这个fn就是一个依赖(effect),需要在obj.prop被重新赋值的时候,再次调用来保证更新;
- 因为需要在获取响应式对象的属性过程中,收集这个依赖fn,因此必须获取到当前执行函数fn的引用;
- 解决方法是:把函数作为effect()的参数包装起来执行,然后在包装器里面获取到函数的引用,进行依赖收集。
1.5 track(obj, property)的实现
track(obj, property)运行后:
- 从targetMap中查找obj,如果没有就创建一个新的Map并赋值给obj对应的值,并作为depsMap返回。如果已经存在,则找到对应的depsMap;
- 在找到的depsMap中,查找property。如果没有则创建一个新的Set,赋值给property对应的值作为dep,并返回这个dep。如果已经存在,则返回找到的Dep;
- 在dep中加入全部effect。
1.6 trigger(obj, property)的实现
trigger(obj, property)运行后:
- 先从targetMap中查找obj,然后找到对应的depsMap;
- 从depsMap中查找property,找出对应的dep;
- 对dep中每一个effect进行执行。
弱映射WeakMap:只能储存对象,且当对象在其他地方没有引用的时候,WeakSet内的对象会被垃圾回收机制识别并回收。
1.7 reactive()函数:将对象变成响应式
Vue使用reactive(obj)函数,将一个对象变成响应式对象。(设置捕获器,返回Proxy对象即可)
官方教程代码如下:(示例,并非源码)
二、为什么要使用Proxy,相比Object.defineProperty有什么好处?
- Object.defineProperty只能劫持对象中已经存在的属性,动态添加的新属性无法劫持(Proxy可以);
- Object.defineProperty不能监听数组变化(Proxy可以);
- Proxy存在兼容性问题,且无法完全polyfill.
三、手动实现Vue响应式代码
// Mars 2021.08
class Dep {
constructor() {
this.dep = new Set();
}
trigger() {
for (let e of this.dep) {
e();
}
}
add(e) {
this.dep.add(e);
}
}
let activeEffect = null;
const objsMap = new WeakMap();
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
function track(obj, prop) {
if (activeEffect !== null) {
if (!objsMap.has(obj)) objsMap.set(obj, new Map());
let depsMap = objsMap.get(obj);
if (!depsMap.has(prop)) depsMap.set(prop, new Dep());
let dep = depsMap.get(prop);
dep.add(activeEffect);
}
}
function trigger(obj, prop) {
if (objsMap.has(obj)) {
if (objsMap.get(obj).has(prop)) {
objsMap.get(obj).get(prop).trigger();
}
}
}
function reactive(obj) {
const handler = {
get(obj, prop) {
track(obj, prop);
return Reflect.get(obj, prop);
},
set(obj, prop, value) {
let oldValue = Reflect.get(obj, prop);
if (oldValue !== value) {
Reflect.set(obj, prop, value);
trigger(obj, prop);
}
return true;
}
};
return new Proxy(obj, handler);
}
// test
let cal = reactive({
a: 12,
b: 13
});
effect(() => {
console.log(cal.a + cal.b);
});