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

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

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

1. 对象访问器属性

将键与两个访问器函数(getset)相关联,以获取或者存储值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对象访问器
let user = {
firstName: "John",
lastName: "Doe",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
[this.firstName, this.lastName] = value.split(" ");
},
};

console.log(user.fullName); // 输出 "John Doe"
user.fullName = "Alice White";
console.log(user.fullName); // 输出 "Alice White"

一个访问器属性,有着以下得特性:

  1. get: 该函数使用一个空的参数列表,以便有权对值执行访问时,获取属性值。参见 getter
  2. set: 使用包含分配值的参数调用的函数。每当尝试更改指定属性时执行。参见 setter
  3. enumerable: 一个布尔值,表示是否可以通过 for...in 循环来枚举属性。另请参阅枚举性和属性所有权,以了解枚举属性如何与其他函数和语法交互。
  4. configurable: 一个布尔值,表示该属性是否可以删除,是否可以更改为访问器属性,并可以更改其特性。

2. Ref

在之前得章节中,我们聊过了 vue3 的响应式核心tracktrigger如果还不了解的朋友,建议看一下前面的文章 🥬 建立一个响应式系统 。闲言少叙,我们直接开始。

简单回顾一下我们的核心内容:

  1. track

track的主要作用是帮助我们创建对应的映射关系,最后帮助我们添加依赖,weakMap的 key 存储对象地址,weakMap 的 value 是一个MapMap的 key 存储的是对象的属性,Map的 value 存储的是一个Set集合,Set集合内部存储的就是我们该对象依赖的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 响应式对象 -> 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);
}
  1. trigger

trigger的主要作用根据对象和 key 在存储的映射关系中,找到并运行相关的依赖。

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

2.1 Ref 原理实现

Vue3 的做法是借助 ref将基础数据类型值进行一层封装,如果想读取数据的话,通过 refReturn.value,如果想设置新值的话,也通过 refReturn.value。接下来直接看伪代码,很简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function ref(raw) {
let oldVal;
const r = {
get value() {
// 收集依赖
track(r, "value");
return raw;
},
set value(newVal) {
// salvePrice.value = newVal
// 将新值newVal传递给raw
oldVal = raw;
raw = newVal;
if (oldVal !== raw) {
// 触发依赖
trigger(r, "value");
}
},
};
return r;
}

2.2 Ref 源码解读

  1. 第一步简简单单调用createRef函数,见名知意,创建一个Ref
1
2
3
function ref(value) {
return createRef(value, false);
}
  1. 第二步简简单单,先判断是不是 ref,如果是直接返回,不是则new RefImpl
1
2
3
4
5
6
7
function createRef(rawValue, shallow) {
// 查看rawValue是不是Ref,要是Ref直接返回
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
  1. 第三步简简单单,检查对象是否带有__v_raw属性,最后返回普通对象
1
2
3
4
5
function toRaw(observed) {
// 检查对象是否带有__v_raw
const raw = observed && observed["__v_raw"];
return raw ? toRaw(raw) : observed;
}
  1. 第四步简简单单,如果传递过来的是对象,直接使用reactive变成响应式对象
1
2
const toReactive = (value) =>
shared.isObject(value) ? reactive(value) : value;
  1. 第五步简简单单,判断是否是shallowRef浅层的 ref
1
2
3
4
function isShallow(value) {
// __v_isShallow是shallowRef的标识符
return !!(value && value["__v_isShallow"]);
}

有人会问,为什么这些东西就是标识符的呢?你是怎么知道的呢?下面给大家扩展一下,我们就用isReadonly举例子:

image.png
从上图中我们可以看出来,value被断言成Target类型。

然后我们在继续看Target对象类型
image.png

这样就很明显了把,我们要从 value 对象中取到ReactiveFlages.IS_READONLY类型的属性对应的boolean值,而ReactiveFlages.IS_READONLY类型是一个枚举:
image.png

所以__v_isReadonlyreadonly的标识符。如果想看具体简洁的可以去看isRef

  1. 判断是否是readonly只读的
1
2
3
4
function isReadonly(value) {
// __v_isReadonly 是readonly的标识符
return !!(value && value["__v_isReadonly"]);
}

上面的代码(3-6)其实都是一些辅助函数,不过不要担心,下面的主要内容也是简简单单,一看就会:

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
class RefImpl {
constructor(value, __v_isShallow) {
// false -> ref true -> shallowRef
this.__v_isShallow = __v_isShallow;
this.dep = void 0;
this.__v_isRef = true; // ref函数__v_isShallow===false, toRaw是将响应式对象变成普通对象,普通对象进入raw直接返回
this._rawValue = __v_isShallow ? value : toRaw(value); // toReactive判断传进去的数据是否是对象,如果是对象交给reactive,反之直接返回
this._value = __v_isShallow ? value : toReactive(value);
}

get value() {
// 收集依赖
trackRefValue(this); // 外界读取.value的时候,将this._value返回 obj.value取的就是this._value
return this._value;
}

set value(newVal) {
// shallowRef readobly 查看是不是其中之一
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal); // 比较新旧两个值是不是同一个值或者对象,不是同一个则进入if代码块内部
if (shared.hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = useDirectValue ? newVal : toReactive(newVal); // 执行依赖函数
triggerRefValue(this, newVal);
}
}
}