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();
});
}
【在线案例演示地址】
|