Vue3 nextTick原理解析

608 阅读4分钟

在使用Vue​的时候,最让人着迷的莫过于nextTick​了,它可以让我们在下一次DOM​更新循环结束之后执行延迟回调。

所以我们想要拿到更新的后的DOM​就上nextTick​,想要在DOM​更新之后再执行某些操作还上nextTick​,不知道页面什么时候挂载完成依然上nextTick​。

使用方法

第一种:传入一个回调函数,在这个回调函数里对 Dom 进行操作

nextTick(() => {
   // 所要进行的 Dom 操作
});

第二种:使用 async 和 await。await nextTick() 之后的都为异步代码

const test = async () => {
	...... // 同步代码
	await nextTick();
	// 异步代码,所要进行的 Dom 操作
	......
};

nextTick源码解析


const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null

export function nextTick<T = void, R = void>(
  this: T,
  fn?: (this: T) => R,
): Promise<Awaited<R>> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

nextTick 的实现细节

在我们页面调用 nextTick​ 的时候,会执行该函数,把我们的参数 fn​ 赋值给 p.then(fn)​,在队列的任务完成后,fn 就执行了

由于加了几个维护队列的方法,所以执行顺序是这样的:

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

queueJob()

该方法负责维护主任务队列,接受一个函数作为参数,为待入队任务,会将参数 push​ 到 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.
  if (
    !queue.length ||
    !queue.includes(
      job,
      isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
    )
  ) {
    // 可以入队就添加到主任务队列
    if (job.id == null) {
      queue.push(job)
    } else {
	// 否则就删除
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    // 创建微任务
    queueFlush()
  }
}
queueFlush()

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

// 是否正在刷新
let isFlushing = false;

// 是否有任务需要刷新
let isFlushPending = false;

// 刷新任务队列
function queueFlush() {
    // 如果正在刷新,并且没有任务需要刷新
    if (!isFlushing && !isFlushPending) {
      
        // 将 isFlushPending 设置为 true,表示有任务需要刷新
        isFlushPending = true;
      
        // 将 currentFlushPromise 设置为一个 Promise, 并且在 Promise 的 then 方法中执行 flushJobs
        currentFlushPromise = resolvedPromise.then(flushJobs);
    }
}
  • ​queueFlush​是一个用来刷新任务队列的方法
  • ​isFlushing​表示是否正在刷新,但是不是在这个方法里面使用的
  • ​isFlushPending​表示是否有任务需要刷新,属于排队任务
  • ​currentFlushPromise​表示当前就需要刷新的任务

flushJobs方法详解

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

  • 先处理前置任务队列
  • 根据 Id​ 排队队列
  • 遍历执行队列任务
  • 执行完毕后清空并重置队列
  • 执行后置队列任务
  • 如果还有就递归继续执行
// 任务队列
const queue = [];

// 当前正在刷新的任务队列的索引
let flushIndex = 0;

// 刷新任务
function flushJobs(seen) {
    // 将 isFlushPending 设置为 false,表示当前没有任务需要等待刷新了
    isFlushPending = false;
  
    // 将 isFlushing 设置为 true,表示正在刷新
    isFlushing = true;
  
    // 非生产环境下,将 seen 设置为一个 Map
    if ((process.env.NODE_ENV !== 'production')) {
        seen = seen || new Map();
    }
  
    // 刷新前,需要对任务队列进行排序
    // 这样可以确保:
    // 1. 组件的更新是从父组件到子组件的。
    //    因为父组件总是在子组件之前创建,所以它的渲染优先级要低于子组件。
    // 2. 如果父组件在更新的过程中卸载了子组件,那么子组件的更新可以被跳过。
    queue.sort(comparator);
  
    // 非生产环境下,检查是否有递归更新
    // checkRecursiveUpdates 方法的使用必须在 try ... catch 代码块之外确定,
    // 因为 Rollup 默认会在 try-catch 代码块中进行 treeshaking 优化。
    // 这可能会导致所有警告代码都不会被 treeshaking 优化。
    // 虽然它们最终会被像 terser 这样的压缩工具 treeshaking 优化,
    // 但有些压缩工具会失败(例如:https://github.com/evanw/esbuild/issues/1610)
    const check = (process.env.NODE_ENV !== 'production')
        ? (job) => checkRecursiveUpdates(seen, job)
        : NOOP;
  
    try {
        for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
            const job = queue[flushIndex];
            if (job && job.active !== false) {
                if ((process.env.NODE_ENV !== 'production') && check(job)) {
                    continue;
                }
              
                // 执行任务
                callWithErrorHandling(job, null, 14 /* ErrorCodes.SCHEDULER */);
            }
        }
    }
    finally {
        // 重置 flushIndex
        flushIndex = 0;
      
        // 快速清空队列,直接给 数组的 length属性 赋值为 0 就可以清空数组
        queue.length = 0;
      
        // 刷新生命周期的回调
        flushPostFlushCbs(seen);
      
        // 将 isFlushing 设置为 false,表示当前刷新结束
        isFlushing = false;
      
        // 将 currentFlushPromise 设置为 null,表示当前没有任务需要刷新了
        currentFlushPromise = null;
      
        // pendingPostFlushCbs 存放的是生命周期的回调,
        // 所以可能在刷新的过程中又有新的任务需要刷新
        // 所以这里需要判断一下,如果有新添加的任务,就需要再次刷新
        if (queue.length || pendingPostFlushCbs.length) {
            flushJobs(seen);
        }
    }
}
flushPreFlushCbs()

该方法负责执行前置任务队列

export function flushPreFlushCbs(
  instance?: ComponentInternalInstance,
  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) {
      if (instance && cb.id !== instance.uid) {
        continue
      }
      if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
        continue
      }
      queue.splice(i, 1)
      i--
      cb()
    }
  }
}
flushPostFlushCbs()

该方法负责执行后置任务队列

export function flushPostFlushCbs(seen?: CountMap) {
  if (pendingPostFlushCbs.length) {
    const deduped = [...new Set(pendingPostFlushCbs)].sort(
      (a, b) => getId(a) - getId(b),
    )
    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()
    }

    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      const cb = activePostFlushCbs[postFlushIndex]
      if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
        continue
      }
      if (cb.active !== false) cb()
    }
    activePostFlushCbs = null
    postFlushIndex = 0
  }
}