01 Mini-vue: Reactivity Base
文章首发于:01 Mini-vue Reactivity Base -Aiysosis Blog(aiysosis.ink)
前言
从这里开始正式开始mini-vue的编写。原课程的代码中是没有Ts类型注解的,在我编写的过程中会尽量地加入类型声明和类型注解,也算是对Ts类型系统学习成果的检验。
本次要实现的是Vue最核心的部分之一——响应性原理。由于之前已经介绍过基础原理:一个例子看懂Vue响应式原理 -Aiysosis Blog(aiysosis.ink),这里就话不多说,直接开干!
相关环境搭建:TS项目初始化(Vitest+Eslint+Prettier) -Aiysosis Blog(aiysosis.ink)
架构
总体架构如图所示,主要有由两个函数构成,分别是reactive
和effect
。
reactive函数接受一个对象,对其进行get
方法和set
方法的代理,返回一个代理过后的对象。get方法中,会基于被访问属性对当前的activeEffect进行依赖收集,存入特定的存储结构中,进行依赖收集我们用一个函数track
来描述;set方法中会对被访问属性相关的依赖进行依赖触发,依赖触发使用函数trigger
来描述。
effect函数接受一个处理函数,在内部将其包装成ReactiveEffect类的实例(图中橙色的部分),并且设置为activeEffect,由依赖收集函数收集。同时fn被放入实例的run
方法中,在effect函数中会被立即调用一次,后续会在依赖触发时被调用。
当前工程目录
|-- src
|-- reactivity
|-- effect.ts
|-- index.ts
|-- reactive.ts
|-- tests
|-- effect.spec.ts
|-- reactivity.spec.ts
测试用例
effect.spec.ts
effect的测试用例用来检查依赖是否被正确收集与触发,同时用来检查effect函数的一些特点,如传入的处理函数应该被立即执行一次(这也是为了依赖能够被正确收集)。
import { effect } from "../effect";
import { reactive } from "../reactive";
describe("effect", () => {
/**
* 最基本的特性,通过reactive绑定依赖&触发依赖
* 通过effect收集依赖
*/
it("happy path", () => {
const user = reactive({ age: 10 });
let nextAge;
effect(() => {//处理函数,每次调用都更新nextAge的值
nextAge = user.age + 1;
});
expect(nextAge).toBe(11);//立即调用一次(effect的特性)
user.age++;
expect(nextAge).toBe(12);//依赖触发
user.age++;
expect(nextAge).toBe(13);//依赖触发
});
});
reactivity.spec.ts
import { reactive } from "../reactive";
describe("reactivity", () => {
it("happy path", () => {
let person = {
age: {
foo: 20,
},
};
let personRef = reactive(person);//调用reactive函数
expect(personRef).not.toBe(person);//代理对象不是原对象
expect(personRef.age.foo).toBe(20);//可以从代理对象中正确获取值
});
});
主体逻辑
reactive.ts
在这里的逻辑很简单,和架构图中的描述完全一致。在函数定义中使用了Ts泛型的类型推断,使用extends关键词把泛型限定为object的子类型,并且在构建Proxy的时候传入泛型,这样在返回的代理对象中也具有了完全相同的类型提示。
import { track, trigger } from "./effect";
export function reactive<T extends object>(raw: T) {
let proxy = new Proxy<T>(raw, {
get(obj, key: string) {
const res = Reflect.get(obj, key);
track(obj, key);//执行依赖收集
return res;
},
set(obj, key: string, val) {
//顺序很重要!如果不先更新值,后面的trigger会使用旧的值,造成错误
const res = Reflect.set(obj, key, val);//<----------------看这里
trigger(obj, key);//执行依赖触发
return res;
},
});
return proxy;
}
effect.ts
这个部分比较复杂,涉及到存储结构、track函数、trigger函数、ReactiveEffect类以及effect函数本体的实现,这里将依次展示,并在最终给出完整版本。
ReactiveEffect类
class ReactiveEffect {
private _fn: Function;
constructor(fn: Function) {
this._fn = fn;
}
run(this: ReactiveEffect) {
//这里采用了Ts的this类型注解,避免编码时可能出现的错误
activeEffect = this;
this._fn();
}
}
let activeEffect: ReactiveEffect;//创建activeEffect实例(全局)
在完成ReactiveEffect类的定义之后,我们立即实例化activeEffect,在架构图中也不难看出,它应该是一个全局的实例,在track函数和effect函数中都会被访问,且依赖收集实际上也是收集了当前的activeEffect。代码中也可以看出,run方法中会把activeEffect指向当前实例。
effect函数
export function effect(fn: Function) {
let _effect = new ReactiveEffect(fn);
_effect.run();
}
是的,effect函数的内容目前就只有这么一点。首先把传入的函数包装成实例,然后执行run方法,在run方法内部activeEffect被指定为当前实例。
存储结构⭐⭐⭐
存储结构的设计是比较值得思考的。在图中是非常简化的描述,实际上的考量要多很多。如果你提前看了一个例子看懂Vue响应式原理 -Aiysosis Blog(aiysosis.ink),就能理解为什么要采用这样的数据结构。
这里再稍作总结,首先,我们依赖收集的最基本的单位是处理函数,这里被封装成了ReactiveEffect类的实例;一个响应式对象的某个属性可能会对应多个处理函数;同时我们的reactive不仅仅服务于一个对象,可能有多个对象都进行了响应性的转化。那么我们的存储结构至少有这些层次:
|-- targetMap //存储多个对象 -> WeakMap, key为对象
|-- depsMap //存储某个对象的多个属性 -> Map, key为属性名
|-- deps //存储某个属性的多个处理函数 ->Set, 不需要索引,但是要不重复
|-- ReactiveEffect //基本单位(处理函数)
type TargetMap = WeakMap<object, DepsMap>;
type DepsMap = Map<string, Set<ReactiveEffect>>;
const targetMap: TargetMap = new WeakMap();
使用类型声明,我们可以准确地描述出这一层次结构。描述完毕后创建targetMap,它就是图中的紫色圆柱体,是一个全局的公共实例。
有了以上的前提,track函数和trigger函数的编码就只剩下比较简单的逻辑了。
track函数
export function track(target: object, key: string) {
let depsMap = targetMap.get(target);
if (!depsMap) {
//如果没有就初始化
depsMap = new Map();
targetMap.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
//如果没有就初始化
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);//添加当前的activeEffect
}
trigger函数
trigger函数的逻辑无非就是取出函数依次执行,也非常简单。
export function trigger(target: object, key: string) {
let depsMap = targetMap.get(target);
let deps = depsMap.get(key);
for (let effect of deps) {//依次执行
effect.run();
}
}
整体代码
class ReactiveEffect {
private _fn: Function;
constructor(fn: Function) {
this._fn = fn;
}
run(this: ReactiveEffect) {
activeEffect = this;
this._fn();
}
}
let activeEffect: ReactiveEffect;
type DepsMap = Map<string, Set<ReactiveEffect>>;
type TargetMap = WeakMap<object, DepsMap>;
/**
* Data Structure:
* TargetMap: WeakMap -> depsMap: Map -> deps: Set
*/
const targetMap: TargetMap = new WeakMap();
/**
*
* @param target 对象
* @param key 属性名
*/
export function track(target: object, key: string) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);
}
/**
*
* @param target 对象
* @param key 属性名
*/
export function trigger(target: object, key: string) {
let depsMap = targetMap.get(target);
let deps = depsMap.get(key);
//如果有scheduler那么运行scheduler,否则运行run
for (let effect of deps) {
effect.run();
}
}
export function effect(fn: Function) {
let _effect = new ReactiveEffect(fn);
_effect.run();
}
运行测试
圆满结束!🎉🎉🎉👏👏👏