实现 effect 的 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 之后
stop(runner)
obj.prop++
// 依赖再次更新,当时传入的 effect 则不会重新执行
expect(dummy).toBe(2)
// runner 不受到影响
runner()
expect(dummy).toBe(3)
})
实现
我们知道所有的 effect 存在 deps 中,也就是我们的 effects 是在 track
方法进行保存的,那么如果不想让这个 effect 执行,就可以找到 target – key 对应的 deps 中,删除掉我们的 effect 即可。
首先,我们在 ReactiveEffect 实例中去记录我们反向对应的 deps
v1版
// effect.ts
class ReactiveEffect {
// [stop] 反向记录自己对应的 dep 那个 set
deps = []
// other code
}
export function track(target, key) {
// other code ...
dep.add(activeEffect)
// [stop]:反向追踪 activeEffect 的 dep
// 因为一个 activeEffect 可能会对应多个 dep,每个 dep 是一个 set
// 这里我们可以使用一个数组
activeEffect.deps.push(dep)
}
实现 stop 方法
class ReactiveEffect {
// other code ...
// [stop] 这个方法的作用就是去根据 this.deps 删除 this 对应的 effect
stop() {
this.deps.forEach((dep: any) => {
dep.delete(this)
})
}
}
export function stop(runner) {
// [stop] 如何获取到当前所属的 effect 实例呢?
// 这样就可以去调用 stop 方法了
runner.effect.stop()
}
export function effect(fn, options: any = {}) {
// other code
const runner: any = _effect.run.bind(_effect)
// [stop] 在这里挂载一下所属的 effect
runner.effect = _effect
return runner
}
v2版
当运行完单测后会出现问题,这是因为如果使用上面的方法的话,会存在重复收集的问题,例如我们在 stop 后,此时所属的 effect 其实已经清空过了,但是下面我们又对依赖项进行了 getter,也就是 track,那么就会再次将所属的 track 收集起来,那么 stop 删除的元素就等于是重新就加回来了,所以我们需要修改一下代码,加一个状态;
class ReactiveEffect {
// [stop] 该 effect 是否调用过 stop 方法了
// true 未调用 false 调用
active = true
stop() {
// 如果没调用这个方法,去清空所属的 effect
if (this.active) {
this.deps.forEach((dep: any) => {
dep.delete(this)
})
this.active = false
}
}
}
// track 的代码也要改一下
export function track(target, key) {
// 如果该 activeEffect 还没有调用 stop 方法的时候,再去添加依赖和反向收集依赖
if (activeEffect.active) {
activeEffect.deps.push(dep)
dep.add(activeEffect)
}
}
我们再去运行单元测试发现就没问题了
代码优化
将删除依赖的函数作为单独的函数
class ReactiveEffect {
stop() {
cleanupEffect(this)
}
}
// 把清除的逻辑单独作为函数
function cleanupEffect(effect) {
if (effect.active) {
effect.deps.forEach((dep: any) => {
dep.delete(effect)
})
effect.active = false
}
}
onStop 单测
it('onStop', () => {
const obj = reactive({
foo: 1,
})
const onStop = vi.fn()
let dummy
// onStop 是一个函数,也是 effect 的 option
const runner = effect(
() => {
dummy = obj.foo
},
{
onStop,
}
)
// 在调用 stop 的时候,onStop 也会执行
stop(runner)
expect(onStop).toBeCalledTimes(1)
})
实现 onStop
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options)
// [stop] 这里我们 options 会接收一个 onStop 方法
// 其实我们可以将 options 中的所有数据全部挂载在 effect 上面
// extend = Object.assign 封装一下是为了语义化更好
extend(_effect, options)
// other code ...
}
// ./src/shared/index.ts
export const extend = Object.assign
工具函数放在 shared
目录下。
然后在清除的函数中去判断并执行
function cleanupEffect(effect) {
if (effect.active) {
effect.deps.forEach((dep: any) => {
dep.delete(effect)
})
// [onStop] 如果存在 onStop,就去运行 onStop
if (effect.onStop) effect.onStop()
effect.active = false
}
}
跑一下测试,发现也是可以通过的
修复 happy path bug
这个时候我们再全局跑一下所有测试,发现在 effect
的 happy path 中出现了错误
export function track(target, key) {
// 这一行找不到 activeEffect
if (activeEffect.active) {
activeEffect.deps.push(dep)
dep.add(activeEffect)
}
}
改成下面这样就可以了
if (activeEffec && activeEffect.active) {
activeEffect.deps.push(dep)
dep.add(activeEffect)
}

原文始发于微信公众号(WEB大前端):实现 effect 的 stop 功能
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/225207.html