7、带你一步步实现vue3源码之stop

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操作之后自动触发依赖,怎样才能让他停止触发呢?如果我们把当前effecttargetMap中删除呢,当触发依赖的时候找不到相关联的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,这样我们可以看到,这一句又触发了objget请求,又再一次进行了依赖收集,所以接下来赋值的时候才会触发刚刚收集的依赖。

优化代码

首先,我们通过一个全局变量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),所以这时候并不会去收集依赖。

总结

在这一节,我们对之前的trackeffect稍微做了修改,在effect返回runner的时候,我们顺便将effect绑定在它上面,并且在收集依赖的时候,除了将effect存储到对应的deps容器上,顺便将deps挂载在effect上,当我们执行stop(runner)的时候,就可以先通过runner获取到其自身的effect,然后获取effect上挂载的deps,最后我们把这个deps中的effect删除即可。

后来,我们有对依赖收集添加了一个判断条件,通过shouldTrack变量来控制依赖收集的时机,当stop之后,我们要避免effect(fn)内部执行get请求的时候会再去收集依赖。

下一节,我们来添加onStop

猜你喜欢

转载自juejin.im/post/7068454528357498888