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

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

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

1. 前言

在读此类文章之前,需要你掌握 javascript、vue

2. vue 和 js 比较

首先,我们来看一个简单的应用。

1
2
3
4
5
6
7
<script setup>
import { reactive, computed } from 'vue'
const production = reactive({ price: 5.00, quantity: 2 })
const totalPriceWithTax = computed(() => {
  return production.price * production.quantity * 1.03
})
</script>
1
2
3
4
5
6
7
8
<template>
 
<div>Price: {{production.price}}</div>
 
<div>Total: {{ production.price * production.quantity }}</div>
 
<div>Taxes: {{ totalPriceWithTax }}</div>
</template>

如上所示,当我们的price价格发生变化的时候,会导致模板中的<div>Price: {{production.price}}</div><div>Total: {{ production.price * production.quantity }}</div>发生更新。同时计算属性发生变化,会继续更新模板中的<div>Taxes: {{ totalPriceWithTax }}</div>

image.png

是不是感觉 amazing,在数据发生变化的时候 vue 是如何知道更新视图的呢?这好像并不符合我们 js 的工作方式

假如我们在 javascript 中,有价格、数量、统计三个变量。我们需要根据价格和数量,计算出统计的值是多少。

1
2
3
4
5
6
7
8
9
10
11
let price = 5;
let quantity = 2;
let total = price * quantity;

console.log(`total is ${total}`); // total is 10

// 当我们价格发生改变的时候
price = 20;

// 再次查看total
console.log(`total is ${total}`); // total is 10

很明显,我们的 total 总量并没有随着 price 价格改变而进行改变。原因很简单,我们的 javascript 代码是同步的,代码自上而下执行,当价格发生改变的时候,并不会影响到已经执行过的let total = price * quantity结果。

那么 vue 是如何实现响应式的呢?接下来我们一起,一步步的深入探索。

3. 响应式

3.1 初识 track、effect、trigger

假如说我们用某一个存储方式,将let total = price * quantity(effect)存储起来。在我们价格发生变化的时候,我们在执行一次存储起来的代码。这样我们的 total 就会随着价格的改变而发生变化了。
在这里我们用set来存储 effect,用 set 的好处就是:他不允许我们重复添加 effect。

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
let price = 5;
let quantity = 2;
let total = 0;
const dep = new Set(); // 用于存储effect
const effect = () => {
total = price * quantity;
};

// 将effect添加到存储仓库中
function track() {
dep.add(effect);
}

// 触发器-一旦运行触发器,他会重新计算我们的总数
function trigger() {
dep.forEach((effect) => effect());
}

track();
effect();

console.log(`total is ${total}`); // 10
// 更改price价格
price = 10;

// 目前没有改变
console.log(`total is ${total}`); // 10
// 执行dep中所有的effect
trigger();

console.log(`total is ${total}`); // 20

到达了这里,是不是感觉代码很神奇,很好玩呢 🍒,不要掉以轻心哟,后面的代码会越来又有意思。到了这里,请大家思考一个问题:通常我们的对象将具有多个属性,并且每个都需要自己的 dep 存储和一组 effect,因此我们将如何存储每个属性的 dep 呢?

3.2 trigger 和 track 的进化

🆗~想必大家都已经想到了,我们可以使用map来存储,key 是对象的属性,value 对应的是该属性的 effect 集合。废话不多说,直接上代码。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 完善一下之前的track和trigger
const depsMap = new Map();
/**
* 我们尝试一下,能不能通过某个属性改变,然后运行相应的effect
* 我们将要监听对象的属性,作为map的key。对应的value是我们之前的set集合
*/
function track(key) {
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}

function trigger(key) {
let dep = depsMap.get(key);
if (dep) {
dep.forEach((effect) => effect());
}
}

回到我们之前的代码中…

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
let product = { price: 5, quantity: 2 };
let total = 0;

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

const depsMap = new Map();
function track(key) {
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}

function trigger(key) {
let dep = depsMap.get(key);
if (dep) {
dep.forEach((effect) => effect());
}
}

track("price");
track("quantity");
effect();

console.log(`total is ${total}`); // 10
// 更改了价格
product.price = 10;

console.log(`total is ${total}`); // 10
trigger("price");

console.log(`total is ${total}`); // 20

我们一起来放松一下,来一个小小的总结,起初我们发现 js 是同步执行的,副作用代码并不会随着值得改变而发生更改。所以我们当时得解决方案是什么呢?没错就是将我们得副作用代码(effect)存储起来。当我们依赖的值发生改变的时候,我们在存储中取出 effect 执行一遍。OK,然后我们就发现了一个新的问题,我们的对象有多个属性,这个时候我们是不是要想办法让他们之间建立连接呢。没错,我们用了map,map 的 key 存储的是对象的属性,value 就是该属性对应的副作用函数集合。 哇塞,感觉好极了。到现在为止我们已经掌握了一种可以追踪对象不同属性的依赖趋势。但是现在又有一个难题,相信聪明的你已经想到了:如果我们存在多个响应式对象又该怎么办呢???

3.3 trigger 和 track 超进化

我们先丰富一下列子,增加一个 user 对象。

1
2
let product = { price: 5, quantity: 2 };
let user = { firstName: "Chen", lastName: "Jie" };

在我们之前的基础上,需要在往前映射一下对象,让我们的对象和属性在关联起来。这里我想到了weakMap弱映射。所谓的weakMap只是一个映射,他的 key 是对象。举个简单的小栗子:

1
2
3
const targetMap = new WeakMap();
targetMap.set(product, "test");
console.log(targetMap.get(product)); // test

创建一个 map,key 是每一个响应式对象。value 则是该对象对应的 map,还记着我们之前的 map 用来存储什么嘛?没错,map 的 key 是该对象的属性,map 的 value 则是该对象属性依赖的副作用函数(effect)集合。

image.png

🆗~还是老规矩,我们继续上代码,丰富 track 和 trigger。

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
// 响应式对象 -> Map
const targetMap = new WeakMap();

function track(target, key) {
// 当前响应式对象是否建立映射关系
let depsMap = targetMap(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}

// 当前对象的属性,是否建立映射关系
let dep = depsMap.get(key);

if (!dep) {
depsMap.set(key, (dep = new Set()));
}

// 添加依赖
dep.add(effect);
}

function trigger(target, key) {
let depsMap = targetMap.get(target); // 检查当前响应式对象是否存在依赖,不存在则直接退出
if (!depsMap) return;
let dep = depsMap.get(key); // 该对象的属性存在依赖函数,循环运行effect
if (dep) {
dep.forEach((effect) => effect());
}
}

不要忘记我们之前的代码:

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
let product = { price: 5, quantity: 2 };
let total = 0;
let user = { name: "John", age: 30 };
let effect = () => {
total = product.price * product.quantity;
};

// weakmap的key可以是对象
let targetMap = new WeakMap();
function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
// 映射当前对象和对象属性的关系
targetMap.set(target, (depsMap = new Map()));
} // 查看当前对象的属性依赖函数effect是否存在

let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
} // 添加当前对象下的属性依赖的effect函数
dep.add(effect);
}

function trigger(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) return;
let dep = depsMap.get(key);
if (dep) {
dep.forEach((effect) => effect());
}
}

// 现在可以将整个对象交给track
track(product, "quantity");
effect();

console.log(`total is ${total}`); // 10
product.quantity = 3;

trigger(product, "quantity");

console.log(`total is ${total}`); // 15

又是一次紧张刺激的进步,到目前为止我们还没有办法让他像 vue 已经自动收集依赖,以及自动触发 trigger 函数。如果 JS 基础很扎实的朋友,到现在的话已经想到了解决办法。对象属性被访问的时候,运行 track 方法进行依赖收集。改变对象属性的时候,运行 trigger 方法。执行副作用函数。对的没错,这就是我们下一章要讲述的内容。🍒 自动收集依赖以及触发 effect,每天进步一点点,我们下期见ヾ(•ω•)o