Mini-vue 源码实战01 响应性核心

92 阅读4分钟

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)

架构

总体架构如图所示,主要有由两个函数构成,分别是reactiveeffect

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();
}

运行测试

圆满结束!🎉🎉🎉👏👏👏