前言 📝
本文将从 0 到 1 手撕一个 mini Reactive,看不会你来打我!

从 0 到 1 手撕 Reactive 响应式教程导航 🚥🚥🚥

  1. 🥬 建立一个响应式系统
  2. 🍒 自动收集依赖以及触发 effect ⇦ 当前位置 🪂
  3. 🎋 Ref响应式

在上一章结尾,我们简单讨论了,如何自动的触发tracktrigger。所以现在的问题就变成了,我们应该如何很好的拦截对象,做一些操作。在 vue3 中,采用了 ES6 的Proxy代理拦截。我们简单的介绍一下ProxyReflect的工作方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let product = { price: 5, quantity: 2 };

const proxyProduct = new Proxy(product, {
// get 拦截
get(target, key, receiver) {
console.log("当前对象的属性" + key);
return Reflect.get(target, key, receiver);
},
// set 拦截
set(target, key, value, receiver) {
console.log("当前要给对象的" + key + "属性赋值为" + value);
return Reflect.set(target, key, value, receiver);
},
});

// 我们可以在控制台调试
proxyProduct.quantity = 4; // 我们会发现proxy代理中的set拦截器函数执行了
console.log(proxyProduct.quantity); // 同样的发现proxy代理中的get拦截器函数执行了

1. Reactive 函数

简单的开篇之后,我们直接进入今天的正题。

在认识了代理和反射之后,我们直接创建一个reactive函数,该函数返回的是代理对象。然后我们在代理对象的getset拦截器函数中做一些我们想做的事情。废话不多说,直接敲代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function reactive(target) {
let hanlder = {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver); // track我们将收集依赖映射(当前对象weakmap -> 属性map -> effect(set集合))
track(target, key);
return res;
},
set(target, key, value, receiver) {
// 获取到之前的数值
let oldVal = target[key];
let result = Reflect.set(target, key, value, receiver);
if (result && oldVal !== value) {
// 更新
trigger(target, key);
}
return result;
},
};
return new Proxy(target, hanlder);
}

哇哦,真是越看这段代码越舒服,这段代码帮助我们解决了很大的问题。现在让我们在回到之前的代码,对之前的代码进行优化。

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
let product = reactive({ price: 5, quantity: 2 });
let total = 0;
let effect = () => {
total = product.price * product.quantity;
};
let targetMap = new WeakMap();

/**
* 当执行函数的时候
* product.price 触发了 reactive中的get函数,在get中触发了track。然后形成了product(weakmap) -> price(map) -> effect(set)
* product.quantity 触发了 reactive中的get函数,在get中触发了track。然后形成了product(weakmap) -> quantity(map) -> effect(set)
*/
effect();

console.log(total); // 10
/**
* 改变数量
* 触发了set函数,set函数中触发了trigger函数
*/
product.quantity = 10;

console.log(total); // 50
// 改变价格
product.price = 10;
console.log(total); // 100
// 收集依赖
function track(target, key) {
//...
}

// 更新
function trigger(target, key) {
// ...
}

function reactive(target) {
let hanlder = {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver); // track我们将收集依赖映射(当前对象weakmap -> 属性map -> effect(set集合))
track(target, key);
return res;
},
set(target, key, value, receiver) {
// 获取到之前的数值
let oldVal = target[key];
let result = Reflect.set(target, key, value, receiver);
if (result && oldVal !== value) {
// 更新
trigger(target, key);
}
return result;
},
};
return new Proxy(target, hanlder);
}

我们终于解放了双手,从此不在自己调用 track 和 trigger。在第一次执行 effect 函数的时候,product.priceproduct.quantity分别触发了 Proxy 的 get 拦截器,此时会进入到我们之前写好的 track 函数中,收集依赖。然后如果我们对product.price或者product.quantity进行赋值的时候,会触发到 Proxy 代理中的 set 拦截器,执行 trigger 方法。将该对象下,该属性对应的 effect 集合执行一次。这样就简单的实现了一个基本的响应式系统。

2. activeEffect

为了让我们的reactive函数更接近于 vue 的源码,我们不妨在继续思考一个问题。在 vue 中我们可能是通过 render 函数去改变响应式数据,或者是通过 watch 函数改变响应式数据,在或者是其他一些函数改动响应式数据,那么我们是如何知道的是谁在使用这个属性呢???或者说当某个函数在执行过程中,用到了响应式数据,响应式数据是如何知道那个函数在用自己的呢???

看过前几篇文章的小伙伴不知道有没有一种错觉。哎,我们不是有track收集依赖的函数吗?他会自动帮我们添加effect副作用函数啊,这个副作用函数不就是我们用到了响应式数据的函数吗?确实是这样的,但是请小伙伴在仔细看一下代码,我们当时只是做了映射关系,具体的添加依赖函数我们并没有去做,只是简单的添加了一个 effect 依赖函数。如果我们还有一个 effect 函数怎么办呢?

其实 vue 用了一个很巧妙的办法,你不要直接执行函数,而是把函数交给一个watcherEffect执行,他会设置一个全局的变量,让全局变量记录当前执行的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 设置当前激活的响应effect函数
let activeEffect = null;
function watcherEffect(eff) {
activeEffect = eff;
activeEffect();
activeEffect = null;
}

// 在完善一下我们的track收集依赖函数
function track(target, key) {
// 只有当前是激活的effect才会收集依赖,也就是effect函数中执行的才会收集依赖
if (activeEffect) {
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);
}
}

恭喜你,我们的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
80
81
82
83
84
let product = reactive({ price: 5, quantity: 2 });
let total = 0;
let salePrice = 0;
let targetMap = new WeakMap();
// 设置当前激活的响应effect函数
let activeEffect = null;

watcherEffect(() => {
total = product.price * product.quantity;
});

watcherEffect(() => {
salePrice = product.price * 0.9;
});

console.log(
`Befor updated total(should be 10) = ${total} salePrice(should be 4.5) = ${salePrice}`
);
product.quantity = 3;
console.log(
`After updated total(should be 15) = ${total} salePrice(should be 4.5) = ${salePrice}`
);
product.price = 10;
console.log(
`After updated total(should be 30) = ${total} salePrice(should be 9) = ${salePrice}`
);

/**
以下是响应式核心代码
*/

function watcherEffect(eff) {
activeEffect = eff;
activeEffect();
activeEffect = null;
}

// 收集依赖
function track(target, key) {
// 只有当前是激活的effect才会收集依赖,也就是effect函数中执行的才会收集依赖
if (activeEffect) {
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);
}
}

// 更新
function trigger(target, key) {
// 通过当前对象获取到对应的map(当前对象的属性和vlaue)
let depsMap = targetMap.get(target);
if (!depsMap) return; // 通过对象的属性,获取到当前属性依赖的effect函数(set集合)
let dep = depsMap.get(key);
if (dep) {
dep.forEach((effect) => effect());
}
}

function reactive(target) {
let hanlder = {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver); // track我们将收集依赖映射(当前对象weakmap -> 属性map -> effect(set集合))
track(target, key);
return res;
},
set(target, key, value, receiver) {
// 获取到之前的数值
let oldVal = target[key];
let result = Reflect.set(target, key, value, receiver);
if (result && oldVal !== value) {
// 更新
trigger(target, key);
}
return result;
},
};
return new Proxy(target, hanlder);
}

3. 总结

通过 Proxy 代理中的 get 和 set 拦截对象属性,当在 get 的时候说明在读取属性,需要我们进行对象-属性-依赖函数之间的关系映射,并且要将当前执行的函数添加到该属性对应的 set 集合中。要想知道当前执行的函数是谁,我们通过将要执行的函数交给一个watcherEffect函数,一个巧妙的全局变量,存储当前正在执行的函数,然后将该函数添加到 set 集合中。然后在给响应式对象赋值的时候,会进入到 proxy 的 set 拦截函数,这个时候就会替我们执行trigger函数,执行我们在track时候收集到该属性对应的依赖函数。实现了一个简单的响应式。