实现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中。如下图所示
然后,在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,可以看到出现了报错
这里的报错是说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进行测试,可以发现所有测试都已经通过。
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后,发现报错了😢
原因是什么呢?
注意到这里有一个表达式为 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这部分功能的实现