vue2、vue3 原理解析

Vue2.0 响应式的理解

核心

  • 对象:通过 defineProperty 对对象的已有属性值的读取和修改进行劫持(监视/拦截)
  • 数组:通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持

注意:对象的多次递归,针对数组需要重写数组方法

函数劫持:把函数内部进行重写同时继续调用老的方法,在继承原数组方法时使用到

问题

  • 对象直接新添加的属性或删除已有属性,界面不会自动更新
  • 直接通过下标替换元素或更新 length,界面不会自动更新 arr[1] = {}

缺陷

  • 需要响应式的数据不能是新增属性
  • 实现过程中对于深度嵌套的数据,递归消耗大量性能

Vue3.0 响应式的理解

核心

  • Vue3.0 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。而且作为新标准将受到浏览器厂商重点持续的性能优化。
  • 通过 Reflect(反射):动态对被代理对象的相应属性进行特定的操作.

优点

  • Proxy 支持监听原生数组
  • Proxy 的获取数据,只会递归到需要获取的层级,不会继续递归
  • Proxy 可以监听数据的手动新增和删除
  • 对象内部所有现有的属性都会自动被监视,而且后续添加的属性,一进入对象也被监视!

缺陷

  • 浏览器存在兼容性问题

问题

  • Proxy 只会代理对象的第一层,那么 Vue3 又是怎样处理这个问题的呢?
    判断当前 Reflect.get 的返回值是否为 Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。

  • 监测数组的时候可能触发多次 get/set,那么如何防止触发多次呢?
    我们可以判断 key 是否为当前被代理对象 target 自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行 trigger。

reactive 原理例子

弱映射表

需要用到弱引用映射表记录一下,防止对象已经代理过了或者多次代理同一个对象,即防止多次出现 reactive(object)reactive(proxy);弱引用映射表(es6),使用 get 和 set 方法进行存取

弱映射表定义

1
2
let toProxy = new WeakMap(); //原对象=>被代理对象
let toRaw = new WeakMap(); //被代理对象=>原对象

下面代码位于 createReactive 方法中,因为在 createReactivenew observed,目的就是为了避免多次 new observed

1
2
3
4
5
6
7
8
9
if (toProxy.get(target)) {
return proxy;
}
//如果对象再次被代理,返回原对象
//即判断该对象已经是代理过的,不再次代理
//不需要使用get即不需要获取,直接判断target有无代理过既可
if (toRaw.has(target)) {
return target;
}

多层递归

关键在于 isObject(res) ? reactive(res) : res;,若 res 为对象,需要递归,与 vue2 一上来就递归不同,会判断需要递归再递归

1
2
3
4
5
6
get(target, key, receiver) {
console.log("获取");
let res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res;
// return target[key] 效果等同Reflect.get方法
}

Reflect

observed 的参数 baseHandler 对象,Reflect.get,Reflect.set,Reflect.deleteProperty

reflect 优点:不会报错,而且有返回值,会替代掉 Object 上的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let baseHandler = {
//receiver表示代理后对象
get(target, key, receiver) {
console.log("获取");
let res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res;
//实现多层代理,若res为对象,需要递归
// return target[key] 效果等同Reflect.get方法
},
set(target, key, value, receiver) {
console.log("设置");
let res = Reflect.set(target, key, value, receiver);
return res;
//target[key] = value
//效果等同Reflect.set方法,但是上面会有布尔类型返回值,明确设置成功或者失败
},
deleteProperty(target, key) {
console.log("删除");
return Reflect.deleteProperty(target, key);
}
};
let observed = new Proxy(target, baseHandler);

数组

存在问题,因为会涉及 length 修改,所以如果不屏蔽,会触发两次,即两次视图更新,不需要

即第一次将元素 push 进去,第二次将 length 改成 对应的值,但是第二次是无意义的更新,需要屏蔽

关键:判断是否为新增属性

1
2
3
function hasOwn(target, key) {
return target.hasOwnProperty(key);
}

在 set 中进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
set(target, key, value, receiver) {
let hadKey = hasOwn(target, key);
let oldValue = target[key];
let res = Reflect.set(target, key, value, receiver);
//判断是否新增属性
if (!hadKey) {
console.log("新增属性");
} else if (oldValue !== value) {
//如果修改的不等于原来的,才会执行,即需要手动改 length 才会执行
console.log("修改属性");
}
return res;
//target[key] = value
//效果等同 Reflect.set 方法,但是上面会有布尔类型返回值,明确设置成功或者失败
}

调用:

1
2
3
4
let arr = [1, 2, 3];
let proxyArr = reactive(arr);
proxyArr.push(4); //新增属性
proxyArr.length = 5; //修改属性

依赖收集

  • activeEffectStacks 栈:先进后出,目的让属性和方法关联,形成响应
  • effect 方法:响应式副作用,默认先执行一次,依赖数据变了再执行
  • createReactiveEffect 方法:创建响应式的副作用
  • run 方法:1.让 fn 执行, 2.将 effect 存入栈