Vue的响应式原理

Vue3 的响应式是借助 ES6 新增的 Proxy 和 Reflect 实现的,本文先介绍 Proxy 和 Reflect 的基本操作,再介绍 Vue3 的响应式

Proxy

  1. Proxy 是 ES6 新增的代理对象,借助这个代理对象可以实现对代理对象(不是源对象)的拦截和自定义操作如:对对象属性的读、写、删除等操作进行拦截和自定义,这些操作被称为捕获器(trap),总计有 13 种捕获器。
  2. 基本使用
    • proxyObj 代理对象的键值对与源对象完全相同
    • Proxy 对象接收两个参数target handler,target 是源对象,准备代理的对象,hanler 对象是用于捕获器的对象。
    • 上例中使用的 handler 对象使用了 get 捕获器,代理对象对属性进行读的操作会触发这个捕获器
    • get 捕获器接收三个参数分别是target:源对象,key:源对象的键,receiver:代理对象 proxyObj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj: Record<string, any> = {
name: "cy",
age: 18,
};

const proxyObj = new Proxy(obj, {
get(target, key: string, receiver) {
console.log(target === obj);
console.log(receiver === proxyObj); // true
console.log(`${key}属性被读`);
return target[key];
},
});
console.log(proxyObj.name);
// name属性被读
// cy
  1. 常见的其它捕获器
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
const obj: Record<string, any> = {
name: "cy",
age: 18,
};

const proxyObj = new Proxy(obj, {
get(target, key: string, receiver) {
console.log(`${key}属性被读`);
return target[key];
},
set(target, key: string, value, receiver) {
console.log(`${key}属性被写`);
return (target[key] = value);
},
deleteProperty(target, key: string) {
console.log(`${key}属性被删除了`);
return true;
},
// 拦截in操作符
has(target, key: string) {
return Reflect.has(target, key);
},
});
console.log(proxyObj.name);
proxyObj.age = 24;
console.log(proxyObj.age);
delete proxyObj.age;
console.log("name" in proxyObj);

Reflect

  1. Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handlers 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的,直接使用即可。
  2. 使用场景
    • 最主要的场景是和 Proxy 的捕获器一起使用
    • 代替原来在 Object 上的部分方法
  3. 基本使用
1
2
3
4
5
6
7
8
9
const duck = {
name: "Maurice",
color: "white",
greeting: function () {
console.log(`Quaaaack! My name is ${this.name}`);
},
};

Reflect.has(duck, "color"); // true
  1. 结合 Proxy 基本使用:将上面例子中直接操作源对象改成使用 Reflect 上的方法即可
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
const obj: Record<string, any> = {
name: "cy",
age: 18,
};

const proxyObj = new Proxy(obj, {
get(target, key: string, receiver) {
console.log(`${key}属性被读`);
return Reflect.get(target, key, receiver);
},
set(target, key: string, value, receiver) {
console.log(`${key}属性被写`);
return Reflect.set(target, key, receiver);
},
deleteProperty(target, key: string) {
console.log(`${key}属性被删除了`);
return true;
},
// 拦截in操作符
has(target, key: string) {
return Reflect.has(target, key);
},
});
console.log(proxyObj.name);
proxyObj.age = 24;
console.log(proxyObj.age);
delete proxyObj.age;
console.log("name" in proxyObj);

Vue3 响应式

  1. 响应式定义:所谓的响应式就是某一个状态发生变化时能够自动监视到对应的变化,并且执行某些操作,在 vue 中就是当 reactive 的响应式对象,对象的属性的值发生变化,就会执行页面的渲染操作,导致页面重新渲染。
  2. 设计原则
    • 对象的属性发生变化,执行某些操作,也就是函数,可以借助 Proxy 对象进行属性监听
    • 执行的这些函数区别于普通的函数,也就是说这些函数应该都有对这些属性进行有读的操作,也就是响应式的函数,也就是需要和对应的属性进行绑定,这些函数可以称之为这些响应式属性的依赖,这些函数需要有标识
      • 每一个属性都存在自己的依赖,都会进行依赖存储和依赖执行操作,有自己的属性和两个对应的操作(方法),可以考虑封装一个依赖类 Depend
      • 只有读操作的函数才会是响应式的函数,响应式也就是说,当属性改变的时候,函数就会执行,什么样的函数呢?当然是使用了这些属性的地方(也就是读)才会重新执行
      • 依赖中执行两个操作,一个是收集依赖函数,一个是执行依赖函数,读取这些属性的函数就是依赖函数(只有当属性改变的时候,这些读这些属性的地方才会重新执行渲染)
        • 读属性的地方进行收集依赖函数
        • 写属性的地方进行执行依赖函数
    • 一个对象有许多不同的属性,不同的属性应该都有自己对应的执行函数,所以说属性和对应的依赖应该存在一个映射关系,这里用 Map 来存储对应关系
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
type IFunc = (...res: any[]) => any;
class Depend {
reactiveFns: Set<IFunc>;
constructor() {
// 不应该执行几次响应式函数就添加几次依赖,多次相同的函数应该只添加一次,所以可以用set
this.reactiveFns = new Set<IFunc>();
}
addDepend(fn: IFunc) {
this.reactiveFns.add(fn);
}
// 执行响应式函数
notify() {
this.reactiveFns.forEach((fn) => {
fn();
});
}
}
const obj: Record<string, any> = {
name: "cy",
age: 24,
};

const map = new Map<string, Depend>();
const objProxy = new Proxy(obj, {
get(target, key: string, receiver) {
if (!map.has(key)) {
map.set(key, new Depend());
}
// 执行依赖函数时也需要读取属性 这时候reactiveFn就是null了 不添加依赖
reactiveFn && map.get(key)?.addDepend(reactiveFn);
return Reflect.get(target, key, receiver);
},
set(target, key: string, value, receiver) {
Reflect.set(target, key, value, receiver);
// 修改属性应该执行这些依赖函数
map.get(key)?.notify();
return true;
},
});
let reactiveFn: IFunc | null = null;
function watchFn(fn: IFunc) {
reactiveFn = fn;
// 响应式函数执行 执行期间会进行依赖收集
fn();
// 执行完毕后清
reactiveFn = null;
}
watchFn(function () {
console.log(objProxy.name, "name的依赖函数");
});
watchFn(function () {
console.log(objProxy.age, "age的依赖函数");
});
objProxy.age = 18;
  1. 上述的设计还有值得优化的地方,可以从以下角度进行优化
    • 一个组件有多个对象是响应式的,这里使用 WeakMap 存储所有的映射关系,因为 WeakMap 是弱引用,当响应式对象想销毁时,直接赋值为 null 即可,不会因为 WeakMap 的影响而影响 GC 回收。
    • 对收集依赖的地方进行函数统一封装
    • 对 addDepend 进行优化
    • 响应式对象的映射关系如下图
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
type IFunc = (...res: any[]) => any;
let reactiveFn: IFunc | null = null;
class Depend {
reactiveFns: Set<IFunc>;
constructor() {
// 不应该执行几次响应式函数就添加几次依赖,多次相同的函数应该只添加一次,所以可以用set
this.reactiveFns = new Set<IFunc>();
}
addDepend() {
// 执行依赖函数时也需要读取属性 这时候reactiveFn就是null了 不添加依赖
reactiveFn && this.reactiveFns.add(reactiveFn);
}
// 执行响应式函数
notify() {
this.reactiveFns.forEach((fn) => {
fn();
});
}
}
const obj: Record<string, any> = {
name: "cy",
age: 24,
};
// 组件的总响应式对象 键是一个个响应式对象 值是响应式对象对应的map
const targetMap = new WeakMap<Record<string, any>, Map<string, Depend>>();
function getDepend(target: Record<string, any>, key: string) {
if (!targetMap.has(target)) {
const map = new Map<string, Depend>();
targetMap.set(target, map);
}
const map = targetMap.get(target);
if (!map?.has(key)) {
const depend = new Depend();
map?.set(key, depend);
}
return map?.get(key);
}
const objProxy = new Proxy(obj, {
get(target, key: string, receiver) {
const depend = getDepend(target, key);
depend?.addDepend();
return Reflect.get(target, key, receiver);
},
set(target, key: string, value, receiver) {
Reflect.set(target, key, value, receiver);
// 修改属性应该执行这些依赖函数
const depend = getDepend(target, key);
depend?.notify();
return true;
},
});
function watchFn(fn: IFunc) {
reactiveFn = fn;
// 响应式函数执行 执行期间会进行依赖收集
fn();
// 执行完毕后清
reactiveFn = null;
}
watchFn(function () {
console.log(objProxy.name, "name的依赖函数");
});
watchFn(function () {
console.log(objProxy.age, "age的依赖函数");
});
objProxy.age = 23;
objProxy.name = "tyz";
  1. 封装响应式函数:类似 Vue3 封装一个 reactive 函数,传入一个对象,得到的对象就是响应式的对象
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
type IFunc = (...res: any[]) => any;
let reactiveFn: IFunc | null = null;
class Depend {
reactiveFns: Set<IFunc>;
constructor() {
// 不应该执行几次响应式函数就添加几次依赖,多次相同的函数应该只添加一次,所以可以用set
this.reactiveFns = new Set<IFunc>();
}
addDepend() {
// 执行依赖函数时也需要读取属性 这时候reactiveFn就是null了 不添加依赖
reactiveFn && this.reactiveFns.add(reactiveFn);
}
// 执行响应式函数
notify() {
this.reactiveFns.forEach((fn) => {
fn();
});
}
}

// 组件的总响应式对象 键是一个个响应式对象 值是响应式对象对应的map
const targetMap = new WeakMap<Record<string, any>, Map<string, Depend>>();
function getDepend(target: Record<string, any>, key: string) {
if (!targetMap.has(target)) {
const map = new Map<string, Depend>();
targetMap.set(target, map);
}
const map = targetMap.get(target);
if (!map?.has(key)) {
const depend = new Depend();
map?.set(key, depend);
}
return map?.get(key);
}
function watchFn(fn: IFunc) {
reactiveFn = fn;
fn();
reactiveFn = null;
}
function reactive(obj: Record<string, any>) {
return new Proxy(obj, {
get(target, key: string, receiver) {
const depend = getDepend(target, key);
depend?.addDepend();
return Reflect.get(target, key, receiver);
},
set(target, key: string, value, receiver) {
Reflect.set(target, key, value, receiver);
// 修改属性应该执行这些依赖函数
const depend = getDepend(target, key);
console.log("修改属性------------");

depend?.notify();
return true;
},
});
}
const info = reactive({
name: "cy",
age: 24,
});
watchFn(() => {
console.log(info.name, "info");
});

watchFn(() => {
console.log(info.age, "info");
});
const info2 = reactive({
name: "tyz",
age: 23,
});

watchFn(() => {
console.log(info2.name, "info2");
});
info.name = "tyz";

info2.name = "cy";

Vue的响应式原理
https://sunburst89757.github.io/2022/09/16/reactive/
作者
Sunburst89757
发布于
2022年9月16日
许可协议