Vue.js 3.0的响应式系统原理
Vue.js 响应式回顾
vue.js 3.0 重写了响应式系统,和vue.js 2.x 相比主要变化体现在以下几个方面。
主要变化
-
Vue.js 3.0的响应式系统
底层采用
Proxy对象实现属性监听,在初始化的时候不需要遍历所有的属性再把属性通过
defineProperty()转化成
getter和
setter。
-
如果有
多层属性嵌套的话,只有访问某个属性的时候才会递归处理下一级的属性,所以Vue.js 3.0中的响应式系统性能要比Vue.js 2.x好。
-
默认监听动态添加的属性;
-
默认监听属性的删除;
-
默认监听数组的索引和length属性;
-
可以作为单独的模块使用。
核心函数/方法
-
reactive/
ref/
toRefs/
computed(创建响应式数据或响应式对象)
-
effect(
watch/
watchEffect底层所使用的函数)
-
track(收集依赖)
-
trigger(触发更新)
Proxy对象回顾
重点关注两个小问题
-
问题1: Proxy对象的使用中,set和deleteProperty中需要返回布尔类型的值;在严格模式下,如果返回false的话会出现Type Error的异常。
'use strict'
const target = {
foo: 'xxx',
bar: 'yyy'
}
const proxy = new Proxy(target, {
get(target, key, receiver){
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver){
return Reflect.set(target, key, value, receiver)
},
deleteProperty(target, key){
return Reflect.deleteProperty(target, key)
}
})
proxy.bar = 'zzz'
从上面的Proxy对象的创建我们可以看到,创建时除了要被代理的目标对象以外,我们还需要传递第二个参数,它是一个对象,我们可以叫做处理器或者监听器。其中的get/set/deleteProperty可以分别监听:对属性的访问、赋值以及删除操作。get/set这两个方法最后有一个参数叫receiver,在这里代表的是当前的Proxy对象或者继承Proxy的对象。在获取或设置值的时候使用了Reflect,分别调用了Reflect中的同名get和set这两个方法。Reflect是反射的意思,是es6中新增的成员。是在代码运行期间用来获取或设置对象中的成员。过去es6之前JavaScript中并没有反射,它可以很随意的把一些方法挂载到Object中,如Object.getPrototypeOf()方法,Reflect中也有对应的方法Reflect.getPrototypeOf(),方法的作用是一样的,只是表达语义的问题。如果在Reflect中有对应的Object中的方法,我们都建议使用Reflect中的方法。所以例子中在创建Proxy对象传入的处理器中都是使用了Reflect来操作对象中的成员。Vue.js 3.0中的源码也是使用的这种方式来操作对象的成员的。
-
问题2: 与Proxy和Reflect中使用的receiver相关 Proxy中的receiver:Proxy或者继承 Proxy的对象,它是当前创建的proxy对象或者继承自当前proxy的子对象。 Reflect中的receiver:如果target对象设置了getter,getter中的this指向receiver
const obj = {
get foo() {
console.log(this)
return this.bar
}
}
const proxy = new Proxy(obj, {
get(target, key, receiver) {
if(key === 'bar') {
return 'value - bar'
}
return Reflect.get(target, key, receiver)
}
})
return Reflect.get(target, key)  return Reflect.get(target, key, receiver) 
Vue.js 3.0的响应式源码中,在获取或设置值的时候,都会传入receiver,以防止类似的意外发生。
响应式原理函数/方法的模拟实现
reactive函数模拟实现
原理分析
- 接收一个参数
target,判断这个参数是否是对象,如果不是的话直接返回。 - 创建拦截器对象
handler,设置 get/set/deleteProperty
- 在构建处理get/set时,如果当前处理的属性成员也是一个对象,那么需要递归调用
reactive函数对其进行响应式处理。 get 中收集依赖set中触发更新deleteProperty中触发更新 - 返回
new Proxy(target, handler)代理对象 - 注意??:reactive只能把对象转换成响应式对象
Created with Rapha?l 2.3.0
开始
接收一个 target 参数
判断这个参数 target 是否是对象?
创建拦截器对象 handler,设置 get/set/deleteProperty
返回 new Proxy(target, handler) 代理对象
结束
返回非对象的参数 target 本身
yes
no
function isObject(obj) {
return obj !== null && typeof obj === 'object';
}
function convert(target) {
return isObject(target) ? reactive(target) : target;
}
function hasOwnProperty(key) {
return Object.prototype.hasOwnProperty(key);
}
function hasOwn(target, key) {
return hasOwnProperty.call(target, key);
}
export function reactive(target) {
if (!isObject(target)) return target;
const handler = {
get(target, key, receiver) {
console.log('get', key);
track(target, key);
const result = Reflect.get(target, key, receiver);
return convert(result);
},
set(target, key, value, receiver) {
const oldVal = Reflect.get(target, key, receiver);
let result = true;
if (oldVal !== value) {
result = Reflect.set(target, key, value, receiver);
trigger(target, key);
console.log('set', key, value);
}
return result;
},
deleteProperty(target, key) {
const haskey = hasOwn(target, key);
const result = Reflect.deleteProperty(target, key);
if (haskey && result) {
trigger(target, key);
console.log('delete', key);
}
return result;
},
};
return new Proxy(target, handler);
}
依赖收集
在依赖收集的过程会创建三个集合:
targetMap/
depsMap/
dep
-
targetMap 的作用时用来记录目标对象和一个字典(也就是depsMap),类型是
WeakMap,即弱引用的Map,里面的key是就是我们的target对象。因为是弱引用,当目标对象失去引用之后,可以销毁。
-
targetMap的值是depsMap。又是一个字典,类型是
Map,这个字典中的key是目标对象中的属性名称,值是一个
Set集合。
-
dep 它是一个Set集合,其中存储元素不会重复。它里面存储的是
effect函数。因为我们可以多次调用一个effect,在effect中访问同一个属性,那这个时候该属性会收集多次依赖,对应多个effect函数。
所以通过这种结构,我们可以存储目标对象,目标对象的的属性,以及属性对一个的Effect函数。一个属性可能对应多个函数。那么将来触发更新的时候,我们可以来这个结构中根据目标对象的属性找到effect函数,然后执行。 
收集依赖 effect 和 track 实现原理
-
effect函数:
- 它接收一个函数作为参数callback,
- 在
effect中,首先要执行一次传入的函数参数callback。 - 在
callback中会访问响应式对象属性,收集依赖。 - 在收集依赖的过程中要把callback存储起来,所以要想办法让之后的track函数能够访问到这里的的callback。
- 需要一个在外部定义的变量来记录
callback,这个变量叫做activeEffect,默认值为null。 - 在
effect中把callback存储到activeEffect中 - (callback被调用)依赖收集完毕之后,需要把
activeEffect重置为null,因为收集依赖的时候如果有嵌套属性的话,是一个递归的过程。 effect函数定义如下: let activeEffect = null;
export function effect(callback) {
activeEffect = callback;
callback();
activeEffect = null;
}
-
track函数:它的作用是收集依赖,它需要往targetMap中添加记录effect函数的callback。
track函数接收两个参数,一个是目标对象target,还有一个是要跟踪的属性key。- 需要先去判断
activeEffect,因为最终我们要去保存activeEffect。如果activeEffect为null则直接返回,说明没有要收集的依赖。 - 否则的话,我们要到
targetMap中根据当前的target来找depsMap,因为我们当前的target就是我们targetMap中的键。 - 接下来我们还要继续判断是否找到了
depsMap,因为当前的target可能没有收集过依赖。如果没有找到的话,那么就要为当前的target创建一个对应的depsMap,来存储对应的属性key和dep对象(也就是我们要执行的那些effect函数)。 - 然后再把它添加到
targetMap中。 - 接下来,再要根据属性key,查找对应的
dep对象。在depsMap里边根据属性key作为健来查找dep,判断dep是否存在。dep是一个Set类型的集合,用来存储我们属性对应的那些effect函数。如果没有找到对的话,也要跟上面一样,要创建一个新的dep集合,并且把它添加到depsMap中。 - 把
effect函数添加到dep集合中。dep.add(activeEffect) - 最后还要在代理对象的
get 中来调用一下这个 track 函数,进行收集依赖。 - track(target, key)函数定义,如下:
let targetMap = new WeakMap();
export function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect);
}
-
trigger函数:它的作用是触发更新,它要去targetMap中找到属性对应的的effect函数,然后来执行。
trigger函数接收两个参数,一个是目标对象target,还有一个是要跟踪的属性key。- 在
trigger函数中,我们要根据target在targetMap中找到depsMap,即把target作为键,来targetMap中找到depsMap。depsMap中存储的是我们的属性以及dep集合(键值对)。dep集合存储的就是我们属性对应的那些effect函数。 - 首先要判断是否找到了
depsMap,没有找到直接返回。 - 否则,再根据
key来找对应的dep集合。 - 接着,要判断找到的
dep集合中是否有值。如果有值的话就要遍历dep集合,然后执行它里边的每一个effect函数。 - 最后,还要在代理对象的
set和deleteProperty方法中,调用trigger函数触发更新。 - trigger函数的定义,如下:
export function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
dep.forEach((effect) => {
effect();
});
}
演示案例
import { reactive, effect } from './reactivity';
const obj = {
name: 'huiquan',
age: '18',
message: 'hi',
};
const reactiveObj = reactive(obj);
let himsg = '';
effect(() => {
himsg = `${reactiveObj.name}, i'm ${reactiveObj.age}`;
});
console.log(himsg);
reactiveObj.age = 28;
console.log(reactiveObj.age);
console.log(himsg);
reactiveObj.name = 'gg';
console.log(himsg);
演示结果

以上函数都在一个模块reactivity.js中定义:
function isObject(obj) {
return obj !== null && typeof obj === 'object';
}
function convert(target) {
return isObject(target) ? reactive(target) : target;
}
function hasOwnProperty(key) {
return Object.prototype.hasOwnProperty(key);
}
function hasOwn(target, key) {
return hasOwnProperty.call(target, key);
}
export function reactive(target) {
if (!isObject(target)) return target;
const handler = {
get(target, key, receiver) {
console.log('get', key);
track(target, key);
const result = Reflect.get(target, key, receiver);
return convert(result);
},
set(target, key, value, receiver) {
const oldVal = Reflect.get(target, key, receiver);
let result = true;
if (oldVal !== value) {
result = Reflect.set(target, key, value, receiver);
trigger(target, key);
console.log('set', key, value);
}
return result;
},
deleteProperty(target, key) {
const haskey = hasOwn(target, key);
const result = Reflect.deleteProperty(target, key);
if (haskey && result) {
trigger(target, key);
console.log('delete', key);
}
return result;
},
};
return new Proxy(target, handler);
}
let activeEffect = null;
export function effect(callback) {
activeEffect = callback;
callback();
activeEffect = null;
}
let targetMap = new WeakMap();
export function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect);
}
export function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;
dep.forEach((effect) => {
effect();
});
}
【在线案例演示地址】
|