在使用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
}
}