Vue3-nextTick源码阅读记录【1】

149 阅读6分钟

本文主要记录 nextTick api 源码的阅读理解,尽量说清一个执行调度机制,如果理解vue2 中nextTick的实现,应该也能很快上手,换汤不换药,一些叫法/名词不太一样。

先看看调度任务 SchedulerJob/SchedulerJobs 类型定义:

interface SchedulerJob extends Function {
  id?: number // 调度任务权重 - 用于执行顺序控制
  pre?: boolean // 调度任务是否在 组件更新 update 前执行
  active?: boolean
  computed?: boolean // 关联computed
  allowRecurse?: boolean // 允许递归
  ownerInstance?: ComponentInternalInstance // 组件实例
}

type SchedulerJobs = SchedulerJob | SchedulerJob[] // 任务队列类型

调度任务的执行涉及两个状态字段:

  • isFlushing:正在清空调度任务队列(正在执行调度任务)
  • isFlushPending:清空调度任务队列逻辑 加入微任务队列

全局调度任务队列:

  • queue: SchedulerJob[] = []组件更新前要执行的调度任务队列,flushIndex 记录正在执行的调度任务的索引,控制新的调度任务可以正确加入队列queue,保证正确执行。
  • pendingPostFlushCbs: SchedulerJob[] = []组件更新后才执行的任务队列。
  • activePostFlushCbs: SchedulerJob[] | null = null 组件更新后才执行且正处于执行过程的任务队列,pendingPostFlushCbs 经过去重(Set)的任务,postFlushIndex 记录正在执行的post类型调度任务的索引,控制新的post调度任务可以正确加入队列queue,保证正确执行。

加入微任务队列方法:

// 全局 Promise,用于将调度任务加入微任务队列
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
// 全局 Promise,用于将清空调度任务队列的逻辑加入微任务队列,全局唯一
let currentFlushPromise: Promise<void> | null = null

Vue3异步更新机制直接利用 Promise 机制实现,不像Vue2受限制,需要进行兼容判断使用 MutationObserver/setTimeout/setImmediate

先看看 nextTick api:

export function nextTick<T = void>(
  this: T,
  fn?: (this: T) => void // 调度任务
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise // 获取Promise,如果已经执行清空调度任务队列,使用currentFlushPromise,否则,直接使用全局 resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p // 将nextTick调度任务加入微任务队列
}

第一眼看是没看懂,竟然就这么两行,相比vue2的api,确实少了挺多!!!

简单说明一下:

调用 nextTick ,传入的回调函数 fn (调度任务) ,如果存在 currentFlushPromise (当前已经将清空调度任务队列queue逻辑加入微任务队列),则用此Promise来添加nextTick的调度任务到微任务队列,保证nextTick回调能在queue任务清空后才执行(update之后调用);如果不存在 currentFlushPromise(未将执行清空queue逻辑加入微任务队列),则直接使用 resolvedPromise 将nextTick任务加入微任务队列。

currentFlushPromise 是怎么来的呢?

先看一下queueJob

// 调度任务加入队列queue
export function queueJob(job: SchedulerJob) {
  // the dedupe search uses the startIndex argument of Array.includes()
  // by default the search index includes the current job that is being run
  // so it cannot recursively trigger itself again.
  // if the job is a watch() callback, the search will start with a +1 index to
  // allow it recursively trigger itself - it is the user's responsibility to
  // ensure it doesn't end up in an infinite loop.
  // 队列queue为空 || 队列queue不含此任务job
  if (
    !queue.length ||
    !queue.includes(
      job,
      isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
    )
  ) {
    // flushIndex 记录正在执行调度任务的索引
    // 如果在执行队列清空时有新任务加入,需要添加到flushIndex索引之后,这样才能保证正确执行。
    if (job.id == null) {
      // 队列末尾加入任务 id==null -> infinite
      queue.push(job)
    } else {
      // findInsertionIndex 根据 flushIndex 和 job.id 获取合适的索引位置
      // 根据id值-选择合适位置插入
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    // 加入队列后,需要将清空队列操作逻辑加入微任务中
    queueFlush()
  }
}

queueFlush 类型定义:

function queueFlush() {
  // 如果当前还未处于调度任务队列执行的状态
  // 或者 还未将调度任务清空逻辑加入微任务队列中
  // 根据isFlushing 和 isFlushPending 状态控制
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true // 标识 已加将清空队列操作加入微任务队列中
    currentFlushPromise = resolvedPromise.then(flushJobs) // 加入微任务队列中,并设置 currentFlushPromise
  }
}

当有调度任务 SchedulerJob 加入队列queue中,会默认执行调度任务队列queue的清空操作,使用 resolvedPromise 加操作放入微任务队列,并返回该Promise,用于后续添加微任务。

再来看看 flushJobs:

function flushJobs(seen?: CountMap) {
  isFlushPending = false // 重置isFlushPending状态
  isFlushing = true // 标识正在执行调度任务
  if (__DEV__) {
    seen = seen || new Map()
  }

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child so its render effect will have smaller
  //    priority number) 组件从父组件向子组件更新,父组件总在先于子组件创建
  // 2. If a component is unmounted during a parent component's update,
  //    its update can be skipped. 组件在父组件更新时被卸载,它的更新会被跳过
  // 调度任务排序 - 根据id大小
  queue.sort(comparator)

  // conditional usage of checkRecursiveUpdate must be determined out of
  // try ... catch block since Rollup by default de-optimizes treeshaking
  // inside try-catch. This can leave all warning code unshaked. Although
  // they would get eventually shaken by a minifier like terser, some minifiers
  // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
  // 函数递归调用检查/登记,防止陷入死循环
  // checkRecursiveUpdates 记录同个函数fn执行次数(维护在CountMap<Map<Function,number>>)
  // 如果次数超过阈值 RECURSION_LIMIT = 100,则提示错误
  const check = __DEV__
    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
    : NOOP

  try {
    // 遍历任务队列,执行任务
    // 记录flushIndex
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        if (__DEV__ && check(job)) {
          continue
        }
        // console.log(`running:`, job.id)
        // 调度任务错误处理
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // 还原queue相关状态
    flushIndex = 0
    queue.length = 0

    // 开始执行post任务执行
    // update 在queue队列
    flushPostFlushCbs(seen)

    // 重置执行状态标识 新的任务加入queue可以再次执行清空
    isFlushing = false
    currentFlushPromise = null // 执行队列删除
    // some postFlushCb queued jobs!
    // keep flushing until it drains.
    // 如果还有剩余任务等待执行。嵌套执行,防止任务调度缺失
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs(seen)
    }
  }
}

清空完queue任务队列后,执行 flushPostFlushCbs() 清空 pendingPostFlushCbs 任务队列:

export function flushPostFlushCbs(seen?: CountMap) {
  if (pendingPostFlushCbs.length) {
    // post 任务去重
    const deduped = [...new Set(pendingPostFlushCbs)]
    pendingPostFlushCbs.length = 0    // 清空post任务队列

    // 加入正在执行的post任务队列 activePostFlushCbs
    // #1947 already has active queue, nested flushPostFlushCbs call
    if (activePostFlushCbs) {
      activePostFlushCbs.push(...deduped)
      // activePostFlushCbs 队列已经在清空(任务执行),加入队列即可等待执行
      return
    }

    // 否则需要开启清空activePostFlushCbs(执行任务)
    activePostFlushCbs = deduped
    if (__DEV__) {
      seen = seen || new Map()
    }

    // post任务排序
    activePostFlushCbs.sort((a, b) => getId(a) - getId(b))

    // 开始执行任务,清空队列
    // 记录postFlushIndex
    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      // 函数调用次数检测
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
      ) {
        continue
      }
      // 执行post任务
      activePostFlushCbs[postFlushIndex]()
    }
    // 重置状态
    activePostFlushCbs = null
    postFlushIndex = 0
  }
}

怎么加入 pendingPostFlushCbs 队列 ?主要是组件声明钩子注入。

queuePostFlushCb 定义:

export function queuePostFlushCb(cb: SchedulerJobs) {
  if (!isArray(cb)) {
    // 如果当前还未开始执行 post 任务,或者正在执行post任务的队列中不存在此任务
    if (
      !activePostFlushCbs ||
      !activePostFlushCbs.includes(
        cb,
        cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex
      )
    ) {
      // 就将任务加入到 pendingPostFlushCbs 队列中,等待执行
      pendingPostFlushCbs.push(cb)
    }
  } else {
    // if cb is an array, it is a component lifecycle hook which can only be
    // triggered by a job, which is already deduped in the main queue, so
    // we can skip duplicate check here to improve perf
    // 只有组件声明钩子才会使用数组形式
    // 直接跳过判断条件,全部加入post任务队列中
    pendingPostFlushCbs.push(...cb)
  }
  
  // 执行queue队列清空
  queueFlush(),保证 post任务在 update 之后执行
}

除此之外,还提供一个 flushPreFlushCbs 方法,从队列queue中找到需要在组件更新update前执行的调度任务,执行并从队列中移除,防止重复执行。

flushPreFlushCbs 定义:

export function flushPreFlushCbs(
  seen?: CountMap,
  // if currently flushing, skip the current job itself
  i = isFlushing ? flushIndex + 1 : 0
) {
  if (__DEV__) {
    seen = seen || new Map()
  }
  for (; i < queue.length; i++) {
    const cb = queue[i]
    if (cb && cb.pre) {
      // 找到 pre = true 的任务
      // 校验调用次数
      if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
        continue
      }
      
      // 执行任务并移出队列queue
      queue.splice(i, 1)
      i--
      cb()
    }
  }
}