从 0 到 1 手撕 Reactive 响应式教程导航 🚥🚥🚥
- 🥬 建立一个响应式系统
- 🍒 自动收集依赖以及触发 effect
- 🎋 Ref响应式 effec ⇦ 当前位置 🪂
1. 对象访问器属性
将键与两个访问器函数(get 和 set)相关联,以获取或者存储值。
// 对象访问器
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"
一个访问器属性,有着以下得特性:
get: 该函数使用一个空的参数列表,以便有权对值执行访问时,获取属性值。参见 getter。set: 使用包含分配值的参数调用的函数。每当尝试更改指定属性时执行。参见 setter。enumerable: 一个布尔值,表示是否可以通过for...in循环来枚举属性。另请参阅枚举性和属性所有权,以了解枚举属性如何与其他函数和语法交互。configurable: 一个布尔值,表示该属性是否可以删除,是否可以更改为访问器属性,并可以更改其特性。
2. Ref
在之前得章节中,我们聊过了vue3的响应式核心track、trigger如果还不了解的朋友,建议看一下前面的文章 {% post_link 建立一个响应式系统 🥬 建立一个响应式系统%} 。闲言少叙,我们直接开始。
简单回顾一下我们的核心内容:
track
track的主要作用是帮助我们创建对应的映射关系,最后帮助我们添加依赖,用weakMap的key存储对象地址,weakMap的value是一个Map。Map的key存储的是对象的属性,Map的value存储的是一个Set集合,Set集合内部存储的就是我们该对象依赖的方法
// 响应式对象 -> 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);
}
trigger
trigger的主要作用根据对象和key在存储的映射关系中,找到并运行相关的依赖。
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。接下来直接看伪代码,很简单。
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源码解读
- 第一步简简单单调用
createRef函数,见名知意,创建一个Ref
function ref(value) {
return createRef(value, false);
}
- 第二步简简单单,先判断是不是ref,如果是直接返回,不是则
new RefImpl
function createRef(rawValue, shallow) {
// 查看rawValue是不是Ref,要是Ref直接返回
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
- 第三步简简单单,检查对象是否带有
__v_raw属性,最后返回普通对象
function toRaw(observed) {
// 检查对象是否带有__v_raw
const raw = observed && observed["__v_raw"];
return raw ? toRaw(raw) : observed;
}
- 第四步简简单单,如果传递过来的是对象,直接使用
reactive变成响应式对象
const toReactive = (value) => shared.isObject(value) ? reactive(value) : value;
- 第五步简简单单,判断是否是
shallowRef浅层的ref
function isShallow(value) {
// __v_isShallow是shallowRef的标识符
return !!(value && value["__v_isShallow"]);
}
有人会问,为什么这些东西就是标识符的呢?你是怎么知道的呢?下面给大家扩展一下,我们就用isReadonly举例子:
从上图中我们可以看出来,
value被断言成Target类型。
然后我们在继续看Target对象类型
这样就很明显了把,我们要从value对象中取到ReactiveFlages.IS_READONLY类型的属性对应的boolean值,而ReactiveFlages.IS_READONLY类型是一个枚举:
所以__v_isReadonly是readonly的标识符。如果想看具体简洁的可以去看isRef
- 判断是否是
readonly只读的
function isReadonly(value) {
// __v_isReadonly 是readonly的标识符
return !!(value && value["__v_isReadonly"]);
}
上面的代码(3-6)其实都是一些辅助函数,不过不要担心,下面的主要内容也是简简单单,一看就会:
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);
}
}
}