Vue3 源码解析系列 - effect

146 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

前言

上篇讲了 reactive 方法的作用和源码,这篇我们讲讲副作用函数是什么。

副作用函数的作用

在 Vue3 中是如何追踪数据的变化呢?其中起到作用的就是副作用函数 effect 副作用是一个函数包裹器,在函数被调用前就启动跟踪,而 Vue3 在派发更新时就能准确的找到这些被收集起来的副作用函数,当数据发生更新时再次执行它。

使用

let foo
const counter = reactive({ num: 0 })
effect(() => (foo = counter.num))
// 此时 foo 应该是 0
counter.num = 7
// 此时 foo 应该是 7

现在创建了一个响应式对象 counter,然后创建了一个副作用函数,将 counter.num 赋值给 foo,这是foo会初始化为 0,而 foo 也会被 counter 收集为一个依赖,而 () => (foo = counter.num) 就是它的更新方法。

如果这个时候我对 counter.num 进行赋值 7,会触发 set 陷阱,在set中触发依赖,执行更新方法,也就是 () => (foo = counter.num) 从而使 foo 的值变为 7

effect

effect 文件中有2个全局变量很重要

// packages/reactivity/src/effect.ts
export type Dep = Set<ReactiveEffect> & TrackedMarkers
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

let activeEffect: ReactiveEffect | undefined

targetMap

targetMap 是一个非常重要的变量,它是 WeakMap 类型,从上面的类型就能看出,它是存储了一个 {target -> Key -> Dep} 的链接。

它的值是一个 KeyToDepMapKeyToDepMap 是一个以依赖 Dep 为值的 Map 对象,我们一直在说的依赖收集就是在收集 Dep 类型的 Set 对象。
举一个例子:

targetMap: {  
  // key 是对象,value 是 depsMap  
  {age: 25} : {    
    // key 是对象里边的 key, value 是 dep    
    age: [ ...此处存储一个个依赖 ]  
  }
}

activeEffect

activeEffect 这个变量标记了当前正在执行的副作用,或者也可以理解为 effect 栈中的栈顶元素。 当一个副作用被压入栈时,会将这个副作用赋值给 activeEffect 变量,而当副作用中的函数执行完后该副作用会出栈,并将 activeEffect 赋值为栈的下一个元素。所以当栈中只有一个元素时,执行完出栈后,activeEffect 就会为 undefined。

整体流程

整个追踪的过程大致是这样的,比如我先有一个响应式对象 const target = {age: 25}

  1. 当依赖收集的时候,会在 targetMap 中创建一个键值对, targetMap.set(target, (depsMap = new Map()))
  2. 然后再把 key 值保存到 depsMap 中 depsMap.set('age', new Set()) 这个 new Set() 保存的就是我们的依赖
  3. 当我现在需要派发更新的时候,就通过 target 和它的key age,找到对应的依赖 new Set(),然后在一个个的去执行更新方法。

ReactiveEffect

我们了解完大概的流程后,再看看 effect 的具体实现。

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  const _effect = new ReactiveEffect(fn)
 
  if (!options || !options.lazy) {
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}
  1. 首先通过 类ReactiveEffect,传进去的一个方法 fn,从而创建一个 effect,这个fn就是我们开头例子中的 () => (foo = counter.num)
  2. 然后判断用户是否传了 options,如果 options.lazy 不为 true 时,就先执行一次 effect.run()
  3. 最后把 effect.run 绑定 this 到 effect,并返回 run 方法。

其实到这里就能够猜到一点,我们传进去的 fn 在 effect.run 中会被执行。

再来看下类 ReactiveEffect 的实现

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined
  computed?: ComputedRefImpl<T>
  allowRecurse?: boolean
  private deferStop?: boolean

  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }

  run() {
    if (!this.active) {
      return this.fn()
    }
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      this.parent = activeEffect
      activeEffect = this
      shouldTrack = true

      trackOpBit = 1 << ++effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        initDepMarkers(this)
      } else {
        cleanupEffect(this)
      }
      return this.fn()
    } finally {
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this)
      }

      trackOpBit = 1 << --effectTrackDepth

      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined

      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

到这里 effect 的属性和方法我们就都能看到了,在 run 方法中,设置了 activeEffect 为当前的 effect,并执行了 fn 方法。

每次我们派发更新时都会调用这个run方法,从而更新值。

小结

今天我们了解了 effect 的用法和作用,并了解到了依赖收集到派发更新的具体流程,了解了这些后,下一节我们就能详细看看如何进行依赖收集,和派发更新。