VUE3内核原理简易应用(effect)

519 阅读5分钟

需求

脑坑老王:小头,你把这个需求做一下!

小头同学:大年三十的需求吗?!

脑坑老王:天要下雨,产品要上线,没办法!

小头同学:实现一个effect函数?直接引用vue或者react内核的功能不就可以了?

脑坑老王:现在需要一个框架无关性的函数,也就是说就算脱离vue、react这些mvvm的框架,后期这个功能会被加入到一个更大的平台化产品里,所以需要独立实现。具体的你可以看看下面的例子:

const title = stateCreater("这是初始化的title内容");
effect(()=>{
	console.log("title",title);
})
title = "这是初始化变更后的title内容";

以上代码会首先输出

“这是初始化的title内容”

在赋值后将会自动输出输出:

“这是初始化变更后的title内容”

需求分析

小头同学:上述代码中可以看出,effect函数并没有指定监控title,但却可以在title被赋值的时候直接使用。此处也是整个实现的核心。所以首先要解决的是如何让预定义状态对访问、赋值有所感知。

实现

双向绑定

在VUE3源码中使用了es6中class对象的get、set修饰符,可以实现对类对象内部私有属性的访问。

class State {
    private _state: any = null;
    get value() {
				console.log("获取value");
        return this._state;
    }
    set value(val: any) {
				console.log("设置value");
        this._state = val;
    }
}
const title = new State();
title.value = "这是初始化的内容";
console.log("title", title.value);

以上代码执行后可以在控制台看到响应输出

设置value拦截
获取value拦截
这是初始化的title内容

也就是说通过get、set修饰符可以在普通变量赋值、读取之前进行有计划的代码处理。此处代码中虽然看似没有使用proxy以及defineProperty,但实际上这里只是es6的一个语法糖,经过编译后的typescript代码可以看到:

var State = /** @class */ (function () {
    function State() {
        this._state = null;
    }
    Object.defineProperty(State.prototype, "value", {
        get: function () {
            console.log("获取value");
            return this._state;
        },
        set: function (val) {
            console.log("设置value");
            this._state = val;
        },
        enumerable: false,
        configurable: true
    });
    return State;
}());

由上述代码可见,get和set标记最终被转化为Object.defineProperty,进行双向绑定。

或许只是一种对于defineProperty更优雅的使用方式。

双向绑定拦截

effect本质上是我们状态变更导致的副作用,所以此处只需要在set之前执行我们预定义的事件,就可以实现需求。

接下来的问题是:如何在每次set的时候知道需要执行的effect呢?

当然是在get操作的时候进行绑定,方式也很简单,当get操作被执行的时候,只需要记录当前执行的ActiveEffect就完成了。

当前的ActiveEffect哪里来呢?当然是effect回调函数第一次执行的时候,这里可以将effect函数看做是当前程序执行的背景环境。

故此我们需要两个新的变量来控制全局调用:

//记录当前正在执行的effect函数
let ActiveEffect: ReactiveEffect | null = null;

//状态与effect函数的对应关系,一个状态可能同时被多个effect函数监听
const effectMap = new Map<State, Array<ReactiveEffect>>();

接下来就是创建一个支持双向绑定的状态(此处只是简易实现,不牵扯到泛型,一个any走天下,有兴趣的朋友可以继续深入研究):

class State {
    private _state: any = null;
    constructor(initValue: any) {
        this._state = initValue;
    }
    get value() {
        if (ActiveEffect) {
            let currentEffects = effectMap.get(this) || [];
            currentEffects.indexOf(ActiveEffect) < 0 && currentEffects.push(ActiveEffect);
            effectMap.set(this, currentEffects);
        }
        return this._state;
    }
    set value(val: any) {
        this._state = val;

        if (effectMap.has(this)) {
            effectMap.get(this)?.forEach((fn) => {
                fn.run();
            });
        }
        // this._state = val;
    }
}
function ref(value: any) {
    return new State(value);
}

接下来也就是最关键的effect响应方式,在effect响应中,我们除了要保证effect的正常运行,还需要有解绑的操作,即当我们的程序运行到一定程度,即使状态变更也不再需要effect函数运行。

class ReactiveEffect {
    private fn: () => void;
    constructor(fn: () => void) {
        this.fn = fn;
    }
    run() {
				//在此处记录,哪个effect函数正在执行
        ActiveEffect = this;
        this.fn();
        ActiveEffect = null;
    }

}

小头同学:这样就可以了吧?一下代码运行后就能看到效果了!

脑坑老王:可以是可以,但如果在程序运行中,因为业务原因需要effect函数不再继续有效,那该怎么做呢?

小头同学:今天已经年初二了,我还没去看电影。。。。

脑坑老王:即使在最绝望的时候,也要抱有信念!

小头同学:你说啥?

脑坑老王:没啥,等你写完代码去看了水门桥就懂了,但现在先让effect函数可以失效!

小头同学:。。。。(内心OS:纯净地道的优美传统语句)

小头同学想了想:stop函数的原理基本就是遍历整个effectMap,从当前的state中找出是否包含需要被stop的effect函数。于是就在ReactiveEffet类中增加了如下方法:

    stop() {
        //将自己从状态的依赖列表里删除
        effectMap.forEach((effect, state, arr) => {
            let index = effect.indexOf(this);

            if (index >= 0) {
                const currentEffects = effectMap.get(state);
                currentEffects && currentEffects.splice(index, 1);
                effectMap.set(state, currentEffects || [])
            }
        })
    }
const title = ref("大年初一!规划内容!!!");
const effectTarget = effect(() => {
    console.log("title", title.value);
})
title.value = "大年初二!一定要写完!!!";
//effectTarget.stop();
title.value = "大年初三去看狙击手!!!";

在没有执行stop的情况下,可见输出:

大年初一!规划内容!!!
大年初二!一定要写完!!!
大年初三去看狙击手!!!

后续只要把stop函数启用,title.value再次被赋值,则effect函数不会再执行。

结尾

小头同学:这样就可以了吧?

脑坑老王:嗯,这个简易版看起来已经可以用在简单的地方了。但我推荐你的是水门桥,你怎么订了狙击手的票?

脑坑老王:还在吗?

脑坑老王:还在吗?人呢?

小头同学:(对方无应答)。。。。