vue3 - mini-vue(二)实现effect - runner & scheduler & stop & onStop

113 阅读6分钟

完善effect

实现effect的runner、scheduler、stop、onStop功能

当前的effect的runner、scheduler、stop、onStop,都还算简单就写在一起了,篇幅相对长一点,不过每一模块的实现逻辑并不复杂。

已实现的reactive&effect部分代码

// reactice - 响应式对象
function reactive (raw) {
  return new Proxy(
    raw,
    {
      get (target, key) {
        // todo 收集依赖
        track(target, key)
        return Reflect.get(target, key)
      },
      set (target, key, value) {
        // todo 触发依赖
        trigger(target, key)
        return Reflect.set(target, key, value)
      }
    }
  )
}
// effect - 包装
class ReactiveEffect {
  private _fn: any
  constructor (fn) {
    this._fn = fn
  }
  run () {
    activeEffect = this
    this._fn()
  }
}
// effect - 首次调用
function effect (fn) {
  const _effect = new ReactiveEffect(fn)
  _effect.run()
}
// track - 收集依赖
const targetMap = new Map()
let activeEffect
function track (target, key) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  dep.add(activeEffect)
}
// trigger - 触发依赖
function trigger (target, key) {
  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)
  for (const effect of dep) {
    effect.run()
  }
}

正文开始

1.返回runner

测试用例
it('runner', () => {
  let foo = 10
  // 调用effect(传入fn)返回一个function,成为runner
  const runner = effect(() => {
    foo++
    return 'foo'
  })
  // 开始会调用fn,foo === 11
  expect(foo).toBe(11)
  // 调用runner再次执行fn,并把fn的返回值return出来
  const r = runner()
  // foo === 12
  expect(foo).toBe(12)
  // r === 'foo'
  expect(r).toBe('foo')
})
功能描述

调用effect会返回一个function(返回的function称之为runner),当调用runner,会再次执行传给effect的fn,并且调用fn的时候,会把fn的返回值return出来。

  • 主要实现三个功能
    • 1.调用effect会返回一个runner - return _effect.run.bind(_effect)
    • 2.调用runner再次执行effect的参数fn - _effect.run.bind(_effect)()
    • 3.调用runner的时候,会返回fn的返回值 - return this._fn()
实现runner
// 执行fn实际上就是调用activeEffect实例的run方法,所以effect返回activeEffect的run方法
function effect (fn) {
  const _effect = new ReactiveEffect(fn)
  _effect.run()
  // 因为run方法中涉及到this指针问题,所以绑定this为activeEffect实例对象
  return _effect.run.bind(_effect)
}
// 更新run
run () {
  activeEffect = this
  // 将fn的返回值return出去
  return this._fn()
}

2.scheduler

测试用例
it('scheduler', () => {
  let dummy
  let run: any
  const scheduler = jest.fn(() => {
    run = runner
  })
  const obj = reactive({ foo: 1 })
  const runner = effect(
    () => {
      dummy = obj.foo
    },
    { scheduler } // 传入配置参数 scheduler: function
  )
  // scheduler一开始的时候不会被调用
  expect(scheduler).not.toHaveBeenCalled()
  // dummy === 1意味着一开始执行了fn
  expect(dummy).toBe(1)
  // 响应式对象的值发生变化
  obj.foo++
  // 传入的scheduler方法被调用
  expect(scheduler).toHaveBeenCalledTimes(1)
  // 且dummy没有变
  expect(dummy).toBe(1)
  // 执行run
  run()
  // dummy === 2意味着fn被调用了
  expect(dummy).toBe(2)
})
功能描述
  • 四个功能点 - 主要是触发依赖(trigger)的时候执行的函数可能会发生变化

    • 1.传入effect的第二个参数 - 一个scheduler的fn

      function effect (fn, options: any = {}) {}

    • 2.effect第一次执行的时候还会调用fn - 无需修改

    • 3.当响应式对象发生变化的时候(触发set),不会执行fn,而是scheduler

      // 如果有scheduler,则执行scheduler
      if (effect.scheduler) {
        effect.scheduler()
      } else { // 没有就执行run
        effect.run()
      }
      
    • 4.如果执行runner的时候,会再次执行fn - 无需修改

实现scheduler
// 更新effect
function effect (fn, options: any = {}) {
  // 将scheduler添加到reacttiveEffect实例上
  _effect = new ActiveEffect(fn, options.scheduler)
  _effect.run()
  return _effect.run.bind(_effect)
}
// 更新trigger
function trigger (target, key) {
  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)
  for (const effect of dep) {
    // 如果有scheduler,则执行scheduler
    if (effect.scheduler) {
      effect.scheduler()
    } else { // 没有就执行run
      effect.run()
    }
  }
}
// 更新ReactiveEffect
class ReactiveEffect {
  private _fn: any
  // 添加public关键字让它可以在实例对象上访问
  constructor (fn, public scheduler?) {
    this._fn = fn
  }
  run () {
    activeEffect = this
    this._fn()
  }
}

3.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.stop = 3
  // dummy === 2意味着dummy不再更新了(通过触发依赖更新的,那么将收集的依赖删除就不再更新)
  expect(dummy).toBe(2)
  // 再次调用runner
  runner()
  // dummy === 3意味着dummy再次开始更新
  expect(dummy).toBe(3)
})
it('onStop')
功能描述

当调用stop(传入runner)的时候,将runner对应的effect从dep中删除掉

实现stop
// 更新ReactiveEffect
class ReactiveEffect {
  private fn
  deps = []
  constructor (fn, public scheduler?) {
    this._fn = fn
  }
  run () {
    activeEffect = this
    return this._fn()
  }
  stop () {
    this.deps.forEach(dep: any => {
      // track收集依赖的时候,将所有的依赖(reactiveEffect实例)都存在dep中,通过delete将
      // runner.effect.stop()调用,this指向runner.effect
      dep.delete(this) // 将runner对应的effect从dep中删除掉
    })
  }
}
// 更新track
function track (target, key) {
  let depsMap = targetMaps.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMaps.set(target, depsMap)
  }
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  dep.add(activeEffect)
  // 反向收集dep
  activeEffect.deps.push(dep)
}
// 更新effect
function effect (fn, options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  _effect.run()
  const runner: any = _effect.run.bind(this)
  // 保存effect到runner中
  runner.effect = _effect
  
  return runner
}
// 新增stop
function stop (runner) {
  runner.effect.stop()
}
优化
重构
stop () {
  cleanupEffect(this)
}
// 将删除effect的逻辑抽离出来
function cleanupEffect (effect) {
  effect.deps.forEach((dep: any) => {
    dep.delete(effect)
  })
}
性能

执行stop后,已经将effect清除过了,再次执行的时候不需要再做其他操作

给一个状态标识是否stop

class ReactiveEffect {
  ...
  active: boolean = true
  ...
  stop () {
    // 多次调用stop的时候也只是清空一次
    if (this.active) {
    	cleanupEffect(this)
        this.active = false
    }
  }
}

4.onStop

测试用例
it('onStop', () => {
  const obj = reactive({ foo: 1 })
  const onStop = jest.fn()
  let dummy
  const runner = effect(
    () => {
      dummy = obj.foo
    },
    { onStop } // 给第二个参数
  )
  // 当执行stop之后
  stop(runner)
  // 传入的onStop会被执行,就是stop的回调函数
  expect(onStop).toBeCalledTimes(1)
})
功能描述

当执行stop后,传入的onStop会被执行,就是onStop作为stop的回调函数。

实现onStop
// 更新effect
function effect (fn ,options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  _effect.onStop = options.onStop

  _effect.run()
  const runner = _effect.run.bind(this)
  runner.effect = _effect
  return runner
}
// 更新ReactiveEffect
class ReactiveEffect {
  ...
  onStop?: () => void
  stop () {
    if (this.active) {
      cleanupEffect(this)
  	  if (this.onStop) {
            this.onStop()
  	  }
      this.active = false
    }
  }
  ...
}
优化
重构
// 工具方法
const extend = Object.assign
// 优化
function effect (fn ,options: any = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  // _effect.onStop = options.onStop
  // 可能有多个options选项
  extend(_effect, options)

  _effect.run()
  const runner = _effect.run.bind(this)
  runner.effect = _effect
  return runner
}
// 优化
function track (target, key) {
  let depsMap = targetMaps.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMaps.set(target, depsMap)
  }
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  dep.add(activeEffect)
  // activeEffect是在effect调用时会被赋值,如果是一个单纯的get操作的时候,activeEffect是undefined
  if (!activeEffect) return // 判空
  activeEffect.deps.push(dep) // 反向收集dep
}

effect完整代码

// 全局变量
const targetMap = new Map()
let activeEffect
const extend = Object.assign

class ReactiveEffect () {
  private _fn
  active: boolean = true
  deps = []
  onStop?: () => void
  constructor (fn, public scheduler?) {
    this._fn = fn
    this.scheduler = scheduler
  }
  run () {
    activeEffect = this
    return this.run()
  }
  stop () {
    if (this.active) {
      cleanupEffect(this)
  	  if (this.onStop) {
            this.onStop()
  	  }
   	  this.active = false
    }
  }
}
function effect (fn, options) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  extend(_effect, options)
  _effect.run()
  const runner = effect.run.bind(this)
  runner.effect = _effect
  return runner
}
function track (target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  const dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  dep.add(activeEffect)
  if (!activeEffect) return
  activeEffect.deps.push(dep)
}
function trigger (target, key) {
  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)
  for (const effect of dep) {
    // 如果有scheduler,则执行scheduler
    if (effect.scheduler) {
      effect.scheduler()
    } else { // 没有就执行run
      effect.run()
    }
  }
}
function cleanupEffect (effect) {
  effect.deps.forEach((dep: any) => {
    dep.delete(effect)
  })
}

关于依赖删除

自己回头再看的时候,发现stop删除依赖的时候看到effect.deps.push(dep),突然很懵忘记为啥这么些了(应该是当时学的时候还没彻底理解)。于是画了张图终于搞明白了。 关于stop时清除依赖的操作.png

举个例子说明一下:
传入effect的fn方法有四个依赖项——name、age、wife、hobby,那么这个fn会对应四个依赖集合(dep),因为对应四个key,一个key对应一个依赖集合。
这四个依赖集合会将fn收集进去,且这个fn会反向收集这四个依赖集合。当其中任意一个key变化的时候(例如name),会找到name对应的依赖集合, 并执行这个集合中收集的effectFn以更新页面中的数据(这里会执行 fn 和 fn2 )。
当stop的时候(以fn举例:即调用stop(runner)),执行后当name、age、wife、hobby任何一项发生变化的时候,fn都不会重新调用——message不会更新, 所以我们需要将这四个依赖集合中收集的fn删除掉(于是就有了effect.deps.forEach(dep => dep.delete(effect)),这样当其中任意一个依赖的key 发生变化时,都不会调用这个fn。拿name来说,stop(runner)执行后,会将name对应的依赖集合中的fn删除,但不影响fn1,当name发生变化时,遍历 name的依赖集合并执行,遍历的时候fn已经被删除了,所以message不会更新,但document.body.innerText会更新。

const onePiece = reactice({ name: '路飞', age: '19', wife: '汉库克', hobby: '吃肉' })
let message
const fn = () => {
    message = `${onePiece.name}今年${onePiece.age}岁,他的老婆是${onePiece.wife},他喜欢${onePiece.hobby} `
}
const fn2 = () => {
    document.body.innerText = onePiece.name
}
const runner = effect(fn)
effect(fn2)
// ... 省略其他effect