发布于 

vue3 响应式原理笔记

v2响应式原理

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

  • 对象:其核心是递归 object 的每一个属性,(这也是浪费性能的地方),给每个对象属性增加 gettersetter,当属性发生变化的时候会更新视图

缺点:defineProperty 只能检测到对象自带的属性,无法检测到对象属性的新增删除

后期 v2 提供了 this.$set(target, prop, val) 来确保你新增的对象属性也具有响应式
官方解释:向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = ‘hi’

  • 数组:重写了那些会改变原数组的方法,原因是 defineProperty 不能检测到数组长度的变化,准确的说是通过改变 length 而增加的长度不能监测到(arr.length = newLength 也不会)。比如重写的: push、pop、shift、unshift、sort、reverse、splice 方法,以便数组发生变化后能够给观察者发送通知,并且 push、unshift、splice 会给数组新增元素,我们还需要知道新增的是什么数据,需要对这些新增的数据进行观测。

v3响应式原理

Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。其实就是在对 目标对象 的操作之前提供了 拦截 ,意味着你可以在这层拦截中进行各种操作,修改某些操作的 默认 行为。这样我们可以不直接操作 对象本身 ,而是通过操作对象的 代理对象 来间接来操作对象,来实现数据的响应式。

语法

1
const p = new Proxy(target, handler)

参数一:要使用 Proxy 包装的目标target 对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
参数二: handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

v3 使用了 es6Proxy 来实现数据的响应式,Proxy 消除了之前 v2.x 中基于 Object.defineProperty 的实现所存在的限制:无法监听属性的添加和删除、数组索引和长度的变更

注意

不过v3底层不是直接对target进行如下的简单操作。而是利用es6ReflectReflect是一个内置的对象,它提供拦截JavaScript操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的 详情:Reflect

v3响应式示例

以下是一个将 user 对象变为响应式对象的示例

首先定义一个 user 对象,这是我们的目标对象,也就是 Proxy 的第一个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type userType = {
name?: string
age?: number
hobby?: string
realName: { [key: string]: string }
[key: string]: unknown // 动态添加属性
}

const user: userType = {
name: 'alice',
age: 18,
hobby: 'cook',
realName: {
firstName: 'ren',
lastName: 'pingsheng'
}
}

然后再定义一个 handle 对象,这是 Proxy 的第二个参数,这里面包含 get()、set()、deleteProperty() 方法

这里有一个问题,如果这个对象是个多层对象,Proxy 并不会代理多层对象(只会代理第一层),因为它是 懒代理 ,与 v2.xObject.defineProperty 不同Object.defineProperty 方法是一开始就会对这个多层对象进行递归处理,所以可以拿到,所以这里我们需要加一个判断:如果目标对象的属性 target[prop] 还是个 对象,则对这个属性对象进行递归代理,这样就能读取到深层对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const handler: object = {
// 获取目标对象某个值
get(target: userType, prop: keyof userType) {
console.log('get 方法执行了');
console.log('target 需要取值的目标对象', target);
console.log('prop 设置的目标对象属性的名称', prop);
if (typeof target[prop] === 'object' && target[prop] !== null) {
// 递归代理,只有取到对应值的时候才会代理
return new Proxy(target[prop] as userType, handler)
}
return Reflect.get(target, prop)
},

// 修改或添加新的属性
set(target: userType, prop: keyof userType, val: string) {
console.log('set 方法执行了');
console.log('target 设置属性的目标对象', target);
console.log('prop 设置的属性的名称', prop);
console.log('val 设置的值', val);
if (typeof target[prop] === 'object' && target[prop] !== null) {
// 递归代理,只有取到对应值的时候才会代理
return new Proxy(target[prop] as userType, handler)
}
return Reflect.set(target, prop, val)
},

// 删除目标对象上的某个属性
deleteProperty(target: userType, prop: keyof userType) {
console.log('del 方法执行了');
console.log('target 删除属性的目标对象', target);
console.log('prop 需要删除的属性的名称', prop);
if (typeof target[prop] === 'object' && target[prop] !== null) {
// 递归代理,只有取到对应值的时候才会代理
return new Proxy(target[prop] as userType, handler)
}
return Reflect.deleteProperty(target, prop)
}
}

最后实例化Proxy

1
2
//  proxyUser是代理后的对象。当外界每次对 proxyUser 进行操作时,就会执行 handler 对象上的一些方法。
const proxyUser = new Proxy(user, handler)

测试

获取对象属性

1
2
// 获取 name 属性
console.log('proxyUser.name: ', proxyUser.name); // alice

测试结果显示成功获取到了proxyUser.name属性

1
2
// 获取嵌套对象属性
console.log('proxyUser.realName.firstName: ', proxyUser.realName.firstName); // ren

image-20211015150020974

测试结果显示成功获取到了proxyUser.realName.firstNamename属性,为什么执行了两遍?

第一遍执行传的targetuser,但是过程遇到了以下判断

1
2
3
4
if (typeof target[prop] === 'object' && target[prop] !== null) {
// 递归代理,只有取到对应值的时候才会代理
return new Proxy(target[prop] as userType, handler)
}

以上的意思是:如果target的属性还是一个object且不为null的情况下则进行递归调用取值,此时的targettarget[prop]handler为自身handler对象,所以才会出现为什么执行了两遍

修改、新增对象属性

1
2
3
console.log(proxyUser.name);	// alice
proxyUser.name = 'xiaoming'
console.log(proxyUser.name); // xiaoming

image-20211015151652067

测试修改嵌套对象属性

1
2
3
4
console.log(proxyUser.realName);  // Proxy {firstName: 'ren', lastName: 'pingsheng'}
proxyUser.realName.firstName = 'yan'
proxyUser.realName.lastName = 'xiazhi'
console.log(proxyUser.realName); // Proxy {firstName: 'yan', lastName: 'xiazhi'}

image-20211015152145776

新增一个 gender 属性

1
2
3
4
proxyUser.gender = 'women';
console.log(proxyUser);
// Proxy {name: 'xiaoming', age: 18, realName: {…}, gender: 'women'}

image-20211015154406651

删除对象属性

1
2
delete proxyUser.hobby
console.log('删除操作', proxyUser);

image-20211015154858559

测试结果删除成功

以上就是v3响应式原理最简单的实现

总结

  1. Proxy使用上比Object.defineProperty方便的多
  2. Object.defineProperty只能代理对象上的某个属性,而Proxy代理整个对象
  3. 如果对象内部要全部递归代理,则Proxy可以只在调用时递归,而Object.defineProperty需要在一开始就全部递归,Proxy性能优于Object.defineProperty
  4. 对象新增属性时,Proxy可以监听到,Object.defineProperty监听不到
  5. 数组新增删除修改时,Proxy可以监听到,Object.defineProperty监听不到