【Vue3源码】第二章 effect功能的完善下
前言
上一章节我们实现了effect函数的runner 和 scheduler,这一章我们继续完善effect函数的功能,stop和onstop。
1、实现effect的stop功能
顾名思义,stop就是让effect停下来的函数。那么怎么才能让effect停下呢?很简单,我们触发run方法时,清空targetMap(前几章中我把它比作仓库)中的dep(收集到的ReactiveEffect),那么run方法调用时由于dep中没有ReactiveEffect,自然就无法触发依赖实现响应式啦~
如果你忘了dep是什么看看这张图帮你回忆起来
先看下我们effect.spec.ts文件中stop函数的测试代码
it("stop",() => {
let dummy;
const obj = reactive({prop:1})
const runner = effect(() => {
dummy = obj.prop
})
//修改响应式对象
obj.prop = 2
expect(dummy).toBe(2)
//停止响应式
stop(runner)
// 继续修改响应式对象
obj.prop = 3
// 响应式停止了,为什么没有变化呢?我们来实现stop功能!
expect(dummy).toBe(2)
//stopped effect should still be manually callable
runner()
expect(dummy).toBe(3)
})
根据上文中测试代码,我们开始实现这个多出来的stop函数,它接收的参数是effect的返回值:runner
- 第一步我们先从修改effect函数。
//响应式函数
export const effect = (fn, options: any = {}) => {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
// 给runner一个any类型逃脱类型检查(本文不做类型上的考虑)
const runner :any = _effect.run.bind(_effect);
// 给runner添加一个属性属性effect
runner.effect = _effect
return runner;
};
上面的代码很巧妙的在runner里添加了一个ReactiveEffect实例(_effect),我们的stop函数要接收effect的返回值runner作为参数,而runner又可以把ReactiveEffect实例带进自己的effect属性中。
为什么要这么带呢?
因为runner携带上effect实例作为属性后,就可以在runner中调用ReactiveEffect类中的方法了!
怎么实现呢?
我们接下来就要改造ReactiveEffect类了,写一个stop方法,这样我们就可以在stop函数中通过runner.effect.stop去调用ReactiveEffect类写好的stop方法。
注意stop方法属于ReactiveEffect类,stop函数是独立的函数不要搞混了哦~看下面我们马上修改ReactiveEffect类
- 修改track函数实现反向收集
要实现stop方法和stop函数前,我们需要收集到dep依赖(之前的文字中我把它比作二级分类),然后清空dep(二级分类存放的是最终的货物)收集到的依赖。~
vue3源码又很巧妙的在ReactiveEffect中新增了deps属性,即activeEffect(全局变量)中添加了一个deps属性(下面修改ReactiveEffect类会写)
runner新增了一个effect属性巧妙,activeEffect新增了一个deps属性,他们都巧妙的串联了起来
看到这我不禁感叹:优雅!实在是太优雅了~
let activeEffect; // 现在它又多了个deps属性
//依赖收集
export function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
//浅拷贝反向收集到dep
activeEffect.deps.push(dep)
}
- 修改ReactiveEffect类实现stop方法和stop函数
好了dep都被收集到了activeEffect.deps中,runner也可以通过runner.effect.stop调用stop方法了
那么stop方法和stop函数就可以闪亮登场了~
class ReactiveEffect {
private _fn;
deps = []//新增deps数组
constructor(fn, public scheduler?) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
activeEffect = this; //我们会把实例收集到activeEffect中,所以activeEffect中会有deps属性
return this._fn();
}
stop() {
// 遍历收集到的deps
this.deps.forEach((dep:any) => {
//因为是浅拷贝收集到的dep,所以这里删掉对应的dep就没有了,没有dep(二级分类)自然就无法触发run方法!
dep.delete(this)
});
}
}
stop函数
很简单,去触发实例中的stop方法即可~
// 停止函数
export const stop = (runner) => {
runner.effect.stop()
};
非常的优雅,代码还可以继续优化~
试想一下,我们第一次触发stop函数后是不是已经清空了dep中所有收集到的ReactiveEffect类?
那么我们继续调用stop函数,还有必要继续清空一个空的dep吗??显然是不用的。
为了代码可读性,我们还可以把stop方法中删掉dep的操作抽离成一个函数clearupEffect增加代码可读性
class ReactiveEffect {
private _fn;
active = true; //新增一个变量控制dep是否需要清空
deps = [];
constructor(fn, public scheduler?) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
activeEffect = this;
return this._fn();
}
stop() {
//第一次可以触发
if(this.active) {
// 抽离成clearupEffect
clearupEffect(this);
//清空后再次stop就无法操作了,只有下次触发响应式函数充值active才能继续触发依赖~
this.active = false
}
}
}
//封装
function clearupEffect(effect) {
effect.deps.forEach((dep: any) => {
dep.delete(effect);
});
}
2.实现onStop功能
和之前的scheduler功能一样,onStop也属于effect第二参数的属性之一,当effect传入onStop函数时,它会在stop函数触发时,跟着触发一次。
我们先看测试用例:
it('onStop',() => {
const obj =reactive({
foo:1
})
const onStop = jest.fn()
let dummy;
const runner = effect(
() => {
dummy = obj.foo
},
{
onStop
}
)
stop(runner)
expect(onStop).toBeCalledTimes(1)
})
-
第一步修改ReactiveEffect类
首先我们要在ReactiveEffect类中声明onStop这个属性(函数类型)
并且在stop方法中,我们通过判断是否传了onStop这个属性来调用执行onStop函数
实现起来非常简单
class ReactiveEffect {
private _fn;
active = true;
deps = [];
onStop? :() => void; //声明它是个函数类型
constructor(fn, public scheduler?) {
this._fn = fn;
this.scheduler = scheduler;
}
run() {
activeEffect = this;
return this._fn();
}
stop() {
if(this.active) {
clearupEffect(this);
// 判断onstop是否存在,存在就执行这个函数
if(this.onStop) {
this.onStop()
}
this.active = false
}
}
}
- 第二步修改effect函数让它能接受onStop
import { extend } from "../shared/extend";
//响应式函数
export const effect = (fn, options: any = {}) => {
const _effect = new ReactiveEffect(fn, options.scheduler);
//extend 相当于 Object.assign,我们把options添加到_effect属性里
extend(_effect,options)
_effect.run();
const runner: any = _effect.run.bind(_effect);
runner.effect = _effect;
return runner;
};
vue3源码为了语意化把 Object.assign 改成了 extend 常量
我们在src下新建一个shared文件夹(分享文件夹),并新建一个extend.js文件
代码如下:
export const extend = Object.assign
3.单元测试,effect流程图
好了effect响应式函数的主要功能就基本完成了~
effect的流程图
画这个太累了~不过希望大家会喜欢
下节预告《vue3源码中的readonly功能》