Stop
这个功能应该不用多说什么,看字面意思也能猜个八九不离十了,但作为程序员的我们,思想还是要严谨,老规矩,我们还是一起来看一个单测。
单测
下面,大家跟着单测的代码,逐行的看,其中会有以我自己的理解添加的注释,希望对大家阅读单测有所帮助。
// src/reactivity/effect.spec.ts
describe("effect", () => {
...
it("stop", () => {
let dummy;
// 创建响应式对象obj
const obj = reactive({prop: 1})
// 这就是之前的runner,当再次执行相当于再次执行了fn
const runner = effect(() => {
dummy = obj.prop
})
// 更改响应式对象obj的prop值
obj.prop = 2
// 这时候dummy也会跟着变,到这一步都是常规操作
expect(dummy).toBe(2)
// 这里就是我们今天要实现的,也是一个function,有一个参数,就是我们的runner
stop(runner)
// 我们再次更新响应式对象的值
obj.prop = 3
// 发现dummy这次没变了,这就是我们今天要实现的主要功能点
expect(dummy).toBe(2)
// 再次执行runner,不出意外,dummy再次更新了
runner()
expect(dummy).toBe(3)
})
})
复制代码
通过阅读上面的单测,我们可以得出一下结论
- stop是一个function,
runner
是它唯一的参数。 - 执行
stop(runner)
之后,当响应式对象发生改变,并不会再次执行effect(fn)
中的fn
函数。 - 再次执行
runner()
,dummy
会再次更新。
分析
通过上面的分析,我们都知道stop
之后,会停止更新,也就是停止触发依赖,然而之前我们的程序会在触发set
操作之后自动触发依赖,怎样才能让他停止触发呢?如果我们把当前effect
从targetMap
中删除呢,当触发依赖的时候找不到相关联的effect
,自然就不会触发了。
编码
带着上面得出的结论和我们自己的分析,我们一步步来实现stop
的相关代码逻辑。
1、stop
函数
既然stop
是一个函数,首先,我们先在effect.ts
中导出一个function
。
// src/reactivity/effect.ts
export function stop (runner) {
runner.effect.stop()
}
复制代码
上面的代码,我们调用了runner.effect
,可之前我们返回的runner
上并没有effect
。
2、runner
绑定effect
实例
下面,我们对effect
做一下修改,给返回的runner
上添加effect
。
export function effect (fn) {
const _effect = new ReactiveEffect(fn, options.scheduler)
_effect.run()
// 将当前`effect`实例挂载到runner上面
const runner:any = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
复制代码
这样,stop()
函数实际执行的便是ReactiveEffect
类中的stop
方法了。
3、给ReactiveEffect
类添加一个stop
方法
通过上面一系列操作,我们根据runner
已经关联到对应的effect
。
?> 思考:有了effect,我们要怎么去清空收集当前effect
的容器(deps
)呢?在依赖收集的时候,我们将effect
收集到对应的deps
,这时候,我们顺便将deps
存储到effect
上是不是就可以了。
// src/reactivity/effect.ts
...
export function track (target, key) {
...
activeEffect.deps.push(dep) //将dep存储在当前创建的effect中
}
...
class ReactiveEffect {
...
deps = [] //定义一个deps用来存储收集当前`effect`的deps
...
public stop () {
//在这里我们需要清空当前的effect
cleanUpEffect(this)
}
}
function cleanUpEffect (effect) {
effect.deps.forEach((dep:any) => {
dep.delete(effect)
})
}
...
复制代码
到这里,我们就完成了stop
的基础功能,可是大家思考一个问题,当我们多次调用stop
,每次都会去清空一次我们的deps
,这样肯定多多少少有点影响性能,下面我们就对stop
进行一个简单的优化,添加一个active
状态,让他只清空一次。
// src/reactivity/effect.ts
class ReactiveEffect{
...
active = true //定义一个状态,判断是否已经清空过,默认为true,代表还没有清空
...
}
public stop () {
// 如果还没有清空,我们就执行清空操作
if (this.active) {
cleanUpEffect(this)
// 将状态改成已经清空,下次再执行stop的时候便不会再次执行该操作了
this.active = false
}
}
复制代码
到这里,stop
的功能就全部完成了。
测试结果
TypeError: Cannot read property 'deps' of undefined
46 | }
47 | dep.add(activeEffect)
> 48 | activeEffect.deps.push(dep)
| ^
49 | }
50 |
51 | export function trigger (target, key) {
at track (src/reactivity/effect.ts:48:16)
at Object.foo (src/reactivity/reactive.ts:6:7)
at Object.<anonymous> (src/reactivity/tests/reactive.spec.ts:9:16)
复制代码
这次好像并没有那么顺利,提示我们activeEffect
有可能为undefined
,我们找到对应的代码。
?> 解释:activeEffect
是在effect
中赋值的,而在reactive.spec.ts
中,我们并没有调用effect
,所以activeEffect
自然就是undefined
。
解决问题
如何解决上面的问题呢?如果当它为undefined
的时候,我们就不让它触发依赖收集的相关操作不就是了,毕竟不涉及effect
也就没必要去收集依赖。
// src/reactivity/effect.ts
export function effect (target, key) {
...
if (!activeEffect) return //在这里判断一下
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
复制代码
再次执行测试,所有的测试都可通过,说明我们的问题解决了。
优化代码
下面我们稍微改动一下stop的单测
// src/reactivity/effect.spec.ts
describe("effect", () => {
...
it("stop", () => {
...
// obj.prop = 3
obj.prop ++ //改成这样
...
})
})
复制代码
再次执行测试,这里的stop
又没效果了。
Expected: 2
Received: 3
77 | obj.prop++
78 | // 发现dummy这次没变了,这就是我们今天要实现的主要功能点
> 79 | expect(dummy).toBe(2)
| ^
80 | // 再次执行runner,不出意外,dummy再次更新了
81 | runner()
82 | expect(dummy).toBe(3)
复制代码
我们思考下为什么会这样,仅仅是把obj.prop=3
改成obj.prop++
,按理来说应该是一样的才对,可这里还是触发了依赖。
现在我们就来拆分一下obj.prop++
,其实相当于obj.prop = obj.prop + 1
,这样我们可以看到,这一句又触发了obj
的get
请求,又再一次进行了依赖收集,所以接下来赋值的时候才会触发刚刚收集的依赖。
优化代码
首先,我们通过一个全局变量shouldTrack
来控制当前是否需要收集依赖。
// src/reactivity/effect.ts
let shouldTrack:boolean = false
...
export function track (target, key) {
...
if (!activeEffect) return
if (!shouldTrack) return //添加这一句
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
...
复制代码
处理完依赖收集的逻辑,我们还需要在适当的时候给shouldTrack
进行赋值。
我们都知道无论是effect(fn)
还是给响应式对象赋值触发依赖的时候都会调用这个fn
,而这个fn
便是ReactiveEffect
中的run
,在fn
里面又会触发响应式对象的get
操作去重新收集依赖,所以最佳的处理实际就是当执行run
的时候我们去做下限制。
// src/reactivity/effect.ts
public run () {
if (!this.active) {
// stop状态下
return this._fn()
}
// 正常状态
shouldTrack = true
activeEffect = this
const result = this._fn()
shouldTrack = false
return result
}
复制代码
正常情况,会先将shouldTrack
状态打开(true
),然后执行this._fn()
操作,执行完之后继续关上而当stop
之后将不会有打开shouldTrack
状态的动作,这样执行this._fn()
的时候,内部去收集依赖的时候shouldTrack
其实还是关闭状态(false
),所以这时候并不会去收集依赖。
总结
在这一节,我们对之前的track
和effect
稍微做了修改,在effect
返回runner
的时候,我们顺便将effect
绑定在它上面,并且在收集依赖的时候,除了将effect
存储到对应的deps
容器上,顺便将deps
挂载在effect
上,当我们执行stop(runner)
的时候,就可以先通过runner
获取到其自身的effect
,然后获取effect
上挂载的deps
,最后我们把这个deps
中的effect
删除即可。
后来,我们有对依赖收集
添加了一个判断条件,通过shouldTrack
变量来控制依赖收集的时机,当stop
之后,我们要避免effect(fn)
内部执行get
请求的时候会再去收集依赖。
下一节,我们来添加onStop
。