从0到1实现自己的Mini-Vue3(2)

311 阅读7分钟

实现effect执行返回runner、scheduler选项、stop功能和onStop钩子

在使用effect的过程中,经常需要定制或者额外调整一些effect原有效果的功能。

例如:手动执行effect中的fn,重新指定副作用函数、停止触发fn的执行 和 此时的回调

下面就按照Vue3源码中的逻辑进行简单实现。

1.effect返回runner

首先,添加测试用例来验证后面实现的功能,打开src/reactivity/tests/effect.spec.ts,添加用例

+ it('should return runner when call effect', () => {
+    let foo = 10
+    const runner = effect(() => {
+        foo++
+        return 'foo'
+    })
+    expect(foo).toBe(11)
+
+    const r = runner()
+    expect(foo).toBe(12)
+    expect(r).toBe("foo")
+})

可以看到,执行effect函数的返回值runner时,相当于执行了fn,所以修改effect函数使其能返回其中的fn。不要忘记将其中的this指向fn本身

class ReactiveEffect {
    private _fn
    constructor(fn: any) {
        this._fn = fn
    }
    run() {
        activeEffect = this
+-      return this._fn()
    }
}

export function effect(fn: any) {
    const _effect = new ReactiveEffect(fn)
    _effect.run()
+   return _effect.run.bind(_effect)
}

执行yarn test进行测试,可以发现测试已经通过。

2.effect的scheduler选项

同样添加官方的用例如下,打开src/reactivity/tests/effect.spec.ts

+ it('scheduler', () => {
+     /*
+     1.通过effect的第二个参数给定的一个scheduler的fn
+     2.effect第一次执行的时候,还会执行fn
+     3.当响应式对象set update时 不会执行fn 而是执行scheduler
+     4.如果说当执行runner的时候,会再次执行fn
+      */
+     let dummy
+     let run: any
+     const scheduler = jest.fn(() => {
+         run = runner
+     })
+     const obj = reactive({ foo: 1 })
+     const runner = effect(() => {
+         dummy = obj.foo
+     }, {
+         scheduler
+     })
+     expect(scheduler).not.toHaveBeenCalled()
+     expect(dummy).toBe(1)
+ 
+     obj.foo++
+     expect(scheduler).toHaveBeenCalledTimes(1)
+ 
+     expect(dummy).toBe(1)
+     run()
+     expect(dummy).toBe(2)
+ })

这个用例稍显复杂,但是不用着急,我们一点一点进行分析。

  • 可以看到,scheduler选项对应的是一个函数,是effect的第二个参数的一个key
  • 并且在effect函数执行后也被执行
  • 在effect的fn里面的响应式对象变化时,不会执行fn,而是执行scheduler
  • 执行runner时,重新执行fn

首先,我们需要给effect函数增加一个options对象参数,从中获取到scheduler函数,然后在执行ReactiveEffect的fn时,判断他是否有了scheduler,如果有scheduler,就执行scheduler,否则才执行fn。

对于第4条,因为runner函数只是bind了fn,所以不受影响。

按照上面的思路,打开src/reactivity/effect.ts,做以下修改

class ReactiveEffect {
    private _fn
+   private scheduler
+-  constructor(fn: any, scheduler: any) {
        this._fn = fn
+       this.scheduler = scheduler
    }
    run() {
        activeEffect = this
        return this._fn()
    }
}
export function trigger(target: any, key: any) {
    let depsMap = targetMap.get(target)
    let deps = depsMap.get(key)
    for (const effect of deps) {
    // 如果存在scheduler,则执行scheduler,否则才执行
+-      if (effect.scheduler) {
+-          effect.scheduler()
+-      } else {
+-          effect.run()
+-      }
    }
}
+- export function effect(fn: any, options: any={}) {
+    const scheduler = options.scheduler
+-   const _effect = new ReactiveEffect(fn, scheduler)
     _effect.run()
     return _effect.run.bind(_effect)
}

3.effect的stop功能和onStop回调

同样先拿下来2个简单的用例,打开effect.spec.ts

+ it('stop', () => {
+     let dummy
+     const obj = reactive({ props: 1 })
+     const runner = effect(() => {
+         dummy = obj.prop
+     })
+     obj.prop = 2
+     expect(dummy).toBe(2)
+     stop(runner)
+     obj.prop = 3
+     // obj.prop = obj.prop + 1 触发get操作重新收集依赖,导致stop失效
+     // obj.prop++
+     expect(dummy).toBe(2)
+ 
+     runner()
+     expect(dummy).toBe(3)
+ })
+ // 支持onStop钩子
+ 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)
+ })

在第一个stop用例中,调用stop方法后,改变effect的fn中的依赖项,fn并没有重新执行。但是,执行runner后,又可以正常执行fn

这个逻辑要如何实现呢?

在前面我们已经知道,effect传入fn执行后,会生成一个ReactiveEffect对象_effect,再执行_effect.run方法,也就是fn,并且将_effect赋值给activeEffect。这样一来,在fn执行时,读取内部依赖,触发getter,就可以将_effect存入到对应依赖key的deps数组中。

那么,想让effect的响应式失效,就需要把它从他的依赖项的deps中去掉,这样在trigger时,就找不到对应的effect了,也就不会重新执行副作用函数了。所以,在收集key-> effect时,还需要反向收集依赖,在_effect中收集它的依赖项的deps,保存在_effect.deps中。如下图所示

image.png

然后,在stop函数中就可以获取到runner函数对应的effect,把effect中deps的每一项都去掉它自身。

修改effect.ts如下

class ReactiveEffect {
    private _fn
    scheduler
+   deps = [] // 保存当前effect依赖的key的deps
    constructor(fn: any, scheduler: any) {
        this._fn = fn
        this.scheduler = scheduler
    }
    run() {
        activeEffect = this
        return this._fn()
    }
+    stop() {
+        if (this.active) {
+            this.active = false
+            for (let dep of this.deps) {
+                dep.delete(this)
+            }
+            // stop之后,没有key依赖当前的effect了,可以直接清空deps
+            this.deps.length = 0
+        }
+    }
}

export function track(target: any, key: any) {
    let depsMap = targetMap.get(target)
    // 如果没有初始化,进行初始化
    if (!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    // 属性对应的副作用函数和集合
    let deps = depsMap.get(key)
    if (!deps) {
        deps = new Set()
        depsMap.set(key, deps)
    }
    if (deps.has(activeEffect)) return
    deps.add(activeEffect)
+   activeEffect.deps.push(deps)
}
export function effect(fn: any, options: any = {}) {
    const scheduler = options.scheduler
    const _effect = new ReactiveEffect(fn, scheduler)
    _effect.run()
+-  const runner:any = _effect.run.bind(_effect)
+-  runner.effect = _effect // 把runner对应的effect实例挂载在他的effect属性上
+-  return runner
}


+ export function stop (runner) {
+     runner.effect.stop()
+ }

当然,为了防止stop重复执行,添加active字段,限制当前effect只能执行一次stop方法

执行yarn test effect进行测试,可以发现stop的单元测试已经通过。

接下来看一下onStop的功能。

这个钩子函数会在stop执行时被调用,我们可以把他添加到ReactiveEffect上,在stop执行时,调用它即可。

class ReactiveEffect {
    private _fn
    scheduler
    deps = []
+   onStop?: () => void
    constructor(fn: any, scheduler: any) {...}
    run() {...}
    stop() {
        if (this.active) {
            this.active = false
            for (let dep of this.deps) {
                dep.delete(this)
            }
            // stop之后,没有key依赖当前的effect了,可以直接清空deps
            this.deps.length = 0
+           if (this.onStop) {
+                this.onStop()
+           }
        }
    }
}
    
export function effect(fn: any, options: any = {}) {
    const scheduler = options.scheduler
    const _effect = new ReactiveEffect(fn, scheduler)
+-  _effect.onStop = options.onStop
    _effect.run()
    const runner:any = _effect.run.bind(_effect)
    runner.effect = _effect
    return runner
}

重新执行yarn test effect进行测试,可以发现测试都已经通过。

然后,我们再处理一些额外情况:

1.执行yarn test reactive,可以看到出现了报错

image.png

这里的报错是说activeEffect是undefined,为什么会出现这种问题呢?

可以看到在当前的reactive用例中,只是读取了一下响应式对象obsered.foo的值,并没有调用effect函数,所以就不存在activeEffect对象,所以在收集依赖时需要加上限制,如果activeEffect为空,就不需要收集

describe('reactive',() => {
    it("happy path",() => {
        const original = {foo:1}
        const obsered = reactive(original)
        expect(obsered).not.toBe(original)
        expect(obsered.foo).toBe(1)
    })
})

继续修改effect.ts

export function track(target: any, key: any) {
+   if(!activeEffect) return
    ...
}

重新执行yarn test进行测试,可以发现所有测试都已经通过。

image.png

2.然后是另外一个情况,修改stop用例如下

+ it('stop', () => {
+     let dummy
+     const obj = reactive({ props: 1 })
+     const runner = effect(() => {
+         dummy = obj.prop
+     })
+     obj.prop = 2
+     expect(dummy).toBe(2)
+     stop(runner)
+-    // obj.prop = 3
+     // obj.prop = obj.prop + 1 触发get操作重新收集依赖,导致stop失效
+-    obj.prop++
+     expect(dummy).toBe(2)
+ 
+     runner()
+     expect(dummy).toBe(3)
+ })

执行yarn test后,发现报错了😢

image.png

原因是什么呢?

注意到这里有一个表达式为 obj.prop++,相当于obj.prop = obj.prop + 1

也就是会在effect函数执行后,activeEffect存在的情况下,又进行依赖收集,这样一来,我们的stop方法就失效了。所以,还需要一个字段来标识什么时候应该进行依赖收集

修改effect.ts如下

+ // 标识当前是否应该收集依赖
+ let shouldTrack = false
class ReactiveEffect {
    private _fn
    scheduler
    deps = []
    active = true
    onStop?: () => void
    constructor(fn: any, scheduler: any) {
        this._fn = fn
        this.scheduler = scheduler
    }
    run() {
        // 如果已经stop了,就不需要收集依赖了
        if (!this.active) {
-           activeEffect = this
            return this._fn()
        }
+       // shouldTrack置为true,保证正常的依赖收集
+       shouldTrack = true
+       activeEffect = this
+       const res = this._fn()
+       // 防止影响后面的stop
+       shouldTrack = false
+       return res
    }
    stop() {...}
}
export function track(target: any, key: any) {
    if (!activeEffect) return
+   if (!shouldTrack) return
    let depsMap = targetMap.get(target)
    // 如果没有初始化,进行初始化
    if (!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
    }
    // 属性对应的副作用函数和集合
    let deps = depsMap.get(key)
    if (!deps) {
        deps = new Set()
        depsMap.set(key, deps)
    }
    deps.add(activeEffect)
    activeEffect.deps.push(deps)
}

再次执行yarn test,发现测试全部通过了。

接下来,我们还有一个任务: 优化一下现在的代码

  • 单独封装isTracking方法用来判断是否应该收集依赖
+ function isTracking() {
+     return activeEffect && shouldTrack
+ }

export function track(target: any, key: any) {
-   if (!activeEffect) return
-   if (!shouldTrack) return
+   if (!isTracking()) return
}
  • 清理依赖项的effect单独抽一个方法cleanupEffect
class ReactiveEffect {
    ...
    stop() {
        if (this.active) {
            this.active = false
-           for (let dep of this.deps) {
-               dep.delete(this)
-           }
-           // stop之后,没有key依赖当前的effect了,可以直接清空deps
-           this.deps.length = 0
+           cleanupEffect(this)
            if (this.onStop) {
                this.onStop()
            }
        }
    }
}
+ function cleanupEffect(effect: any) {
+     for (let dep of effect.deps) {
+         dep.delete(effect)
+     }
+     // stop之后,没有key依赖当前的effect了,可以直接清空deps
+     effect.deps.length = 0
+ }
  • 依赖项和effect的双向收集trackEffect
export function track(target: any, key: any) {
    ...
-   if (deps.has(activeEffect)) return
-   deps.add(activeEffect)
-   activeEffect.deps.push(deps)
+   trackEffect(deps)

}
+ function trackEffect(dep) {
+     if (dep.has(activeEffect)) return
+     dep.add(activeEffect)
+     activeEffect.dep.push(dep)
+ }

完成上述优化之后,重新执行yarn test,发现测试一样全部通过了。

至此,就完成了effect这部分功能的实现