Vue3--nextTick源码解析

127 阅读4分钟

nextTick简介

背景:Vue在更新响应式状态时,最终的DOM更新不是同步生效的,是由Vue将它们缓存在一个队列中,直到下一个"tick"才一起执行。目的是确保每个组件无论发生多少次状态改变,都仅执行一次更新。

nextTick()可以在状态改变后立即使用,获取更新后的DOM

nextTick源码解析

入口文件packages/runtime-core/src/sheduler.ts

image.png

nextTick接受一个函数为参数,创建一个微任务。

调用nextTick时,执行该函数,把参数fn赋值给p.then(fn),在对列任务完成后,fn就会执行。

执行顺序是:

queueJob -> queueFlush -> flushJobs -> nextTick参数的 fn

queueJob()

该方法负责维护主任务队列,

const queue: SchedulerJob[] = []
let isFlushing = false // 是否正在执行
let isFlushPending = false // 是否正在等待执行

export function queueJob(job: SchedulerJob) {
  // 判断条件:主任务队列为空 或者 有正在执行的任务且没有在主任务队列中  
  && job 不能和当前正在执行任务及后面待执行任务相同
  // 重复数据删除:
  // - 使用Array.includes(Obj, startIndex) 的 起始索引参数:startIndex
  // - startIndex默认为包含当前正在运行job的index,此时,它不能再次递归触发自身
  // - 如果job是一个watch()回调函数或者当前job允许递归触发,则搜索索引将+1,以允许他递归触发自身-用户需要确保回调函数不会死循环
  if (
    (!queue.length ||
      !queue.includes(
        job,
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      )) &&
    job !== currentPreFlushParentJob
  ) {
    // 判断当前job id 是否存在 不存在则添加到主任务队列
    if (job.id == null) {
      queue.push(job)
    } else {
      // 存在则从当前任务队列中查到位置并删除替换
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    // 创建微任务
    queueFlush()
  }
}

queueFlush()

该方法负责创建微任务,等待任务队列执行

let isFlushing = false // 是否正在执行
let isFlushPending = false // 是否正在等待执行
const resolvedPromise: Promise<any> = Promise.resolve() // 微任务创建器
let currentFlushPromise: Promise<void> | null = null // 当前任务

function queueFlush() {
  // 当前没有微任务
  if (!isFlushing && !isFlushPending) {
    // 避免在事件循环周期内多次创建新的微任务
    isFlushPending = true
    // 创建微任务,把 flushJobs 推入任务队列等待执行
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

flushJobs()

该方法负责处理队列任务主要逻辑如下:

  • 先处理前置任务队列
  • 根据 Id 进行队列排序,并遍历执行队列任务,执行完毕后清空并重置队列
  • 执行后置队列任务
  • 如果队列没有被清空会递归调用flushJobs清空队列
function flushJobs(seen?: CountMap) {
  isFlushPending = false // 是否正在等待执行
  isFlushing = true // 正在执行
  if (__DEV__) {
    seen = seen || new Map() // 开发环境下
  }

  flushPreFlushCbs(seen) // 执行前置任务队列

  // 根据 id 排序队列,这是为了一下两点:
  // 1. 组件更新顺序为:父到子,因为父级总是在子级前面先创建,它的渲染效果具有较小的优先级数
  // 2. 如果父组件更新期间卸载了子组件,则改子组件更新将跳过
  queue.sort((a, b) => getId(a) - getId(b))


  // checkRecursiveUpdate 的条件使用必须在 try ... catch 块外中确定,因为 Rollup 默认会在 try-catch 中取消优化 treeshaking。
  const check = __DEV__
    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
    : NOOP

  try {
    // 遍历主任务队列,批量执行更新任务
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        if (__DEV__ && check(job)) {
          continue
        }
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    flushIndex = 0 // 队列任务执行完,重置队列索引
    queue.length = 0 // 清空队列

    flushPostFlushCbs(seen) // 执行后置队列任务

    isFlushing = false  // 重置队列执行状态
    currentFlushPromise = null // 重置当前微任务为 Null
    // some postFlushCb queued jobs!
    // keep flushing until it drains.
    // 如果主任务队列、前置和后置任务队列还有没被清空,就继续递归执行
    if (
      queue.length ||
      pendingPreFlushCbs.length ||
      pendingPostFlushCbs.length
    ) {
      flushJobs(seen)
    }
  }
}

flushPreFlushCbs(),该方法负责执行前置任务队列

  • 待处理前置任务队列不为空时,备份并清空前置任务队列
  • 并遍历执行待处理前置队列任务,执行完毕后当前任务队列
  • 如果队列没有被清空会递归调用flushJobs清空队列
const pendingPreFlushCbs: SchedulerJob[] = []
let activePreFlushCbs: SchedulerJob[] | null = null
let preFlushIndex = 0

export function flushPreFlushCbs(
  seen?: CountMap,
  parentJob: SchedulerJob | null = null
) {
  // 待处理前置任务队列不为空
  if (pendingPreFlushCbs.length) {
    currentPreFlushParentJob = parentJob
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)] // 待处理前置任务队列去重备份为activePreFlushCbs
    pendingPreFlushCbs.length = 0 // 待处理前置任务队列重置
    if (__DEV__) {
      seen = seen || new Map()
    }
    // 遍历执行队列里的任务
    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
      ) {
        continue
      }
      // 任务执行
      activePreFlushCbs[preFlushIndex]()
    }
    // 清空当前活动的任务队列
    activePreFlushCbs = null 
    preFlushIndex = 0
    currentPreFlushParentJob = null
    // 递归执行,直到清空前置任务队列
    flushPreFlushCbs(seen, parentJob)
  }
}

flushPostFlushCbs(),该方法负责执行后置任务队列

const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0

export function flushPostFlushCbs(seen?: CountMap) {
  // 待处理后置任务队列队列不为空
  if (pendingPostFlushCbs.length) {
    // 待处理后置任务队列去重备份为deduped
    const deduped = [...new Set(pendingPostFlushCbs)]
    pendingPostFlushCbs.length = 0 // 待处理后置任务队列重置

    // #1947 already has active queue, nested flushPostFlushCbs call
    // 如果当前已经有活动的队列,就添加到执行队列的末尾,并返回
    if (activePostFlushCbs) {
      activePostFlushCbs.push(...deduped)
      return
    }
    // 赋值为当前活动队列
    activePostFlushCbs = deduped
    if (__DEV__) {
      seen = seen || new Map()
    }

    // 队列排序
    activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
    // 遍历执行队列里的任务
    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
      ) {
        continue
      }
      activePostFlushCbs[postFlushIndex]()
    }
    // 清空当前活动的任务队列
    activePostFlushCbs = null
    postFlushIndex = 0
  }
}

整个源码解析完成。