需求
脑坑老王:小头,你把这个需求做一下!
小头同学:大年三十的需求吗?!
脑坑老王:天要下雨,产品要上线,没办法!
小头同学:实现一个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函数不会再执行。
结尾
小头同学:这样就可以了吧?
脑坑老王:嗯,这个简易版看起来已经可以用在简单的地方了。但我推荐你的是水门桥,你怎么订了狙击手的票?
脑坑老王:还在吗?
脑坑老王:还在吗?人呢?
小头同学:(对方无应答)。。。。