Vue 3 Pre-Alpha / vue-next 源码学习之 vue/reactivity/effect.ts

前言

前两篇《Vue 3 Pre-Alpha / vue-next 源码学习之 vue/reactivity/reactive.ts》和《Vue 3 Pre-Alpha / vue-next 源码学习之 vue/reactivity/ref.ts》中,我们学习了如何把目标数据或者目标对象封装成了响应式的或者对象,但除用于响应数据读写变化的方法(在 vue 3 中会把用户传入的响应方法/响应函数再次封装,然后成为本篇介绍的 effect 或者叫 ReactiveEffect),我们还没有涉及,现在下面主要对结合一个简单的例子及 effect 的源码进行解读。

一个简单的例子

下面是自己动手,对一个简单的加法(plus)函数通过 createEffect 方法创建出两个新的 effect 函数 plusEffect1plusEffect2,这两个函数跟原始的 plus 一样可以执行加法,但被附加上了一个自己的 options 属性

const run = (fn, args) => {
  return fn(...args)
}
const plus = (a, b) => a + b
const createEffect = (fn, options) => {
  function effect() {
    return run(fn, [...arguments])
  }
  effect.options = options // 在 Vue 3 的 effect 中,通过 options 把 effectSymbol, active, raw, onTrack 等附加进来
  return effect
}
const plusEffect1 = createEffect(plus, { name: 'plusEffect1' })
plusEffect1(1, 2) // => 3
plusEffect1.options.name // => plusEffect1
const plusEffect2 = createEffect(plus, { name: 'plusEffect1' })
plusEffect2(1, 2) // => 3
plusEffect2.options.name // => plusEffect2

源码分析

import { OperationTypes } from './operations' // set, add, delete, clear, get, has, iterate 操作类型
import { Dep, targetMap } from './reactive' // Dep 是 Set<ReactiveEffect>, KeyToDepMap 是 Map<string | symbol, Dep>, targetMap 是 WeakMap<any, KeyToDepMap>
import { EMPTY_OBJ, extend } from '@vue/shared' // 工具函数

export const effectSymbol = Symbol(__DEV__ ? 'effect' : void 0)

export interface ReactiveEffect<T = any> { // effect 的定义
  (): T
  [effectSymbol]: true
  active: boolean
  raw: () => T
  deps: Array<Dep>
  computed?: boolean
  scheduler?: (run: Function) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}

export interface ReactiveEffectOptions { // effect options 的定义
  lazy?: boolean
  computed?: boolean
  scheduler?: (run: Function) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}

export interface DebuggerEvent { // 用于调试
  effect: ReactiveEffect
  target: any
  type: OperationTypes
  key: string | symbol | undefined
}

export const effectStack: ReactiveEffect[] = [] // effect stack 临时数组,当 effect run 完之后就会被 pop 出来

export const ITERATE_KEY = Symbol('iterate') // 使用唯一标识,Symbol 可以创建私有变量作用

export function isEffect(fn: any): fn is ReactiveEffect { // 函数是否为 effect,给 effect 添加了一个私有标识,通过 Symbol 实现
  return fn != null && fn[effectSymbol] === true
}

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ // options 初始为空对象
): ReactiveEffect<T> {
  if (isEffect(fn)) { // 如果函数已经被封装成 effect 就把 raw 函数取出来
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options) // 创建 efect
  if (!options.lazy) { // 如果不是异步执行,马上执行
    effect()
  }
  return effect
}

export function stop(effect: ReactiveEffect) { // 停止事件的 effect,类似于 removeEventListener
  if (effect.active) {
    cleanup(effect)
    if (effect.onStop) { // 有 onStop 勾子,则执行
      effect.onStop()
    }
    effect.active = false // 停止事件的 effect,其实就是把其 active 属性改成 false
  }
}

function createReactiveEffect<T = any>( // 创建一个 effect,其实 effect 就是变种的 fn,根据 options 给 fn 附加一系列的勾子函数及属性
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: any[]): any {
    return run(effect, fn, args)
  } as ReactiveEffect
  effect[effectSymbol] = true
  effect.active = true
  effect.raw = fn
  effect.scheduler = options.scheduler
  effect.onTrack = options.onTrack
  effect.onTrigger = options.onTrigger
  effect.onStop = options.onStop
  effect.computed = options.computed
  effect.deps = []
  return effect
}

function run(effect: ReactiveEffect, fn: Function, args: any[]): any { // 执行 effect / fn 函数
  if (!effect.active) {
    return fn(...args)
  }
  if (!effectStack.includes(effect)) { // 如果这个 effect 还在 effectStack 里面,未执行完,不需要任何操作
    cleanup(effect)
    try {
      effectStack.push(effect) // 入栈 effectStack
      return fn(...args)
    } finally {
      effectStack.pop() // 执行完后,就要把 effect 由 effectStack 中 pop 出来
    }
  }
}

function cleanup(effect: ReactiveEffect) { // 顾名思义,就是清空 effect.deps
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

let shouldTrack = true // 要不要 track,默认是要的

export function pauseTracking() { // 暂停 track
  shouldTrack = false
}

export function resumeTracking() { // 继续 track
  shouldTrack = true
}

export function track( // 跟踪 effect 是由什么操作触发
  target: any,
  type: OperationTypes,
  key?: string | symbol
) {
  if (!shouldTrack || effectStack.length === 0) {
    return
  }
  const effect = effectStack[effectStack.length - 1]
  if (type === OperationTypes.ITERATE) {
    key = ITERATE_KEY
  }
  let depsMap = targetMap.get(target) // depsMap 由 targetMap(WeakMap)中取出
  if (depsMap === void 0) { // 如果 targetMap 里面没有 target 这个 key,
    targetMap.set(target, (depsMap = new Map())) // 则追加 set 进去
  }
  let dep = depsMap.get(key!) // depsMap 是 Map,可能通过 key 取到相应的 dep
  if (dep === void 0) { // 如果 dep 为空,则 set 一个 dep(类型是 Set)
    depsMap.set(key!, (dep = new Set())) // key! 是表示强制解析,表示 key 一定有值
  }
  if (!dep.has(effect)) {
    dep.add(effect) // effect add 到 dep(Set)里面
    effect.deps.push(dep) // 入 deps 栈
    if (__DEV__ && effect.onTrack) { // dev 环境需要触发 onTrack
      effect.onTrack({
        effect,
        target,
        type,
        key
      })
    }
  }
}

export function trigger( // 按操作类型,触发执行 effect
  target: any,
  type: OperationTypes,
  key?: string | symbol,
  extraInfo?: any
) {
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) { // 没被 tracked 过,直接返回,ref 创建 Ref 时,get 操作需要 track,set 操作需要 trigger
    // never been tracked
    return
  }
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  if (type === OperationTypes.CLEAR) {
    // collection being cleared, trigger all effects for target
    depsMap.forEach(dep => {
      addRunners(effects, computedRunners, dep)
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE
    if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
      const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }
  const run = (effect: ReactiveEffect) => { // 注意:此 run 运行完后,是不需要入栈 effectStack,会临时入栈 computedRunners,跟外面那 run 函数不一样
    scheduleRun(effect, target, type, key, extraInfo)
  }
  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  computedRunners.forEach(run) // 运行所有 computed 的effect,
  effects.forEach(run) // 运行所有的 effect
}

function addRunners( // runners 入栈
  effects: Set<ReactiveEffect>,
  computedRunners: Set<ReactiveEffect>,
  effectsToAdd: Set<ReactiveEffect> | undefined
) {
  if (effectsToAdd !== void 0) {
    effectsToAdd.forEach(effect => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
}

function scheduleRun( // 计划执行 effect
  effect: ReactiveEffect,
  target: any,
  type: OperationTypes,
  key: string | symbol | undefined,
  extraInfo: any
) {
  if (__DEV__ && effect.onTrigger) {
    effect.onTrigger(
      extend(
        {
          effect,
          target,
          key,
          type
        },
        extraInfo
      )
    )
  }
  if (effect.scheduler !== void 0) {
    effect.scheduler(effect)
  } else {
    effect()
  }
}

总结

通过对 effect 源码的分析,结合之前对 refreactive 的理解,可以想象得到作者在规划新版的 Vue 3 的 reactivity 一些思路及方向:先对用户传入的状态数据或者状态变量进行二次封装,目标是达到数据的读写都是可被监听的;然后对用户传入的针对数据读写的响应方法进行二次封装,让响应方法的执行也是可被监听的。总之,目标是对数据的读写变化及响应数据的变化而执行相应的事件及方法,整个流程下都是、可控的、可调试、可追踪、可额外加入其他的 Vue 需要的属性及勾子函数等等。

问题思考:couter1 的值发生变化,没有触发第二个 effect 执行,没有改变了 dummy2 的值,这是为什么呢?

\packages\reactivity\__tests__\effect.spec.ts 文件添加一个测试用例

  it('should observe 2 different basic reactive effects', () => {
    let dummy1
    const counter1 = reactive({ num: 0 })
    effect(() => (dummy1 = counter1.num)) // 第一个effect,里面包含 counter1 的读操作
    let dummy2
    const counter2 = reactive({ num: 0 })
    const fnSpy = jest.fn(() => (dummy2 = ++counter2.num))
    effect(fnSpy) // 第二个effect,里面包含 counter2 的读操作
    expect(fnSpy).toHaveBeenCalledTimes(1)

    expect(dummy1).toBe(0)
    expect(dummy2).toBe(1)
    counter1.num = 7
    expect(dummy1).toBe(7) // couter1 的值发生变化,触发第一个 effect 执行,改变了 dummy1 的值
    expect(dummy2).toBe(1) // couter1 的值发生变化,没有触发第二个 effect 执行,没有改变了 dummy2 的值,这是为什么呢?
    expect(fnSpy).toHaveBeenCalledTimes(1) // couter1 的值发生变化,没有触发第二个 effect 执行
  })

实现原理

reactive 对象与 effect 是通过 targetMap 来建立绑定关系的

reactive-对象与-effect-是通过-targetMap-来建立绑定关系的
reactive-对象与-effect-是通过-targetMap-来建立绑定关系的

作者: 博主

Talk is cheap, show me the code!

发表评论

电子邮件地址不会被公开。

Captcha Code