文章目录
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' } // `set`和`deleteProperty`中需要返回布尔类型的值; // 在严格模式下,如果返回`false`的话会出现`Type Error`的异常。 const proxy = new Proxy(target, { get(target, key, receiver){ // return target[key] return Reflect.get(target, key, receiver) }, set(target, key, value, receiver){ // target[key] = value return Reflect.set(target, key, value, receiver) }, deleteProperty(target, key){ // delete target[key] return Reflect.deleteProperty(target, key) } }) proxy.bar = 'zzz' // delete target.foo
从上面的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
// 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) // 这里Reflect.get没有传递receiver, 上面的obj没有bar属性,所以会返回undefined
return Reflect.get(target, key, receiver) // 这里向Reflect.get传递了receiver, 获取foo属性时,foo中的this指向了receiver,也就是当前的创建的proxy, 它的getter中对key值做了判断,所以会返回'value - bar'
}
})
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
中触发更新
- 在构建处理get/set时,如果当前处理的属性成员也是一个对象,那么需要递归调用
- 返回
new Proxy(target, handler)
代理对象 - 注意⚠️:reactive只能把对象转换成响应式对象
// utils functions
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) {
// 收集依赖 Todo
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);
// set and deleteProperty must return a boolean
let result = true;
if (oldVal !== value) {
result = Reflect.set(target, key, value, receiver);
// 触发更新 Todo
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),类型是
WeakMap
,即弱引用的Map,里面的key是就是我们的target对象。因为是弱引用,当目标对象失去引用之后,可以销毁。 -
targetMap的值是depsMap。又是一个字典,类型是
Map
,这个字典中的key是目标对象中的属性名称,值是一个Set
集合。 -
dep 它是一个Set集合,其中存储元素不会重复。它里面存储的是
effect
函数。因为我们可以多次调用一个effect,在effect中访问同一个属性,那这个时候该属性会收集多次依赖,对应多个effect函数。
targetMap
/
depsMap
/
dep
所以通过这种结构,我们可以存储目标对象,目标对象的的属性,以及属性对一个的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中定义:
// utils functions
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) {
// 收集依赖 Todo
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);
// set and deleteProperty must return a boolean
let result = true;
if (oldVal !== value) {
result = Reflect.set(target, key, value, receiver);
// 触发更新 Todo
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();
});
}