文章目录
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();
});
}
本文深入探讨Vue.js 3.0的响应式系统,从Proxy对象回顾到响应式原理的模拟实现,包括依赖收集的机制和更新触发的详细过程。文章通过代码示例和案例演示,清晰地解释了Vue.js 3.0响应式系统的工作方式。

2176

被折叠的 条评论
为什么被折叠?



