nextTick
- 定义:将回调推迟到下一个 DOM 更新周期之后执行,在更改了一些数据以等待 DOM 更新后立即使用它
- 在实际中使用这个方法一般是用于组件更新,你需要获取更新后的数据,所以使用nextTick等待DOM更新
import { createApp, nextTick } from 'vue' const app = createApp({ setup() { const message = ref('Hello!') const changeMessage = async newMessage => { message.value = newMessage // 这里的value是旧值 await nextTick() // nextTick后获取的就是DOM更新后的value console.log('Now DOM is updated') } } })
- 这个api使用时相当简单,而且相当容易理解,但是为了知其意,还是要翻一下源码了解它的执行机制
export function nextTick( this: ComponentPublicInstance | void, fn?: () => void ): Promise<void> { const p = currentFlushPromise || resolvedPromise return fn ? p.then(this ? fn.bind(this) : fn) : p }
- 上面是vue源码中nextTick的实现,为了搞清楚实现逻辑,就必须搞懂
currentFlushPromise
这个变量的含义,所以要从任务的调度机制开始分析
任务调度
首先这个调度机制的功能在runtime-core
的scheduler
文件
API
// 这个文件会抛出以下几个API函数 nextTick(){} // 将函数在任务队列清空后执行 queueJob(){} // 添加任务并开始执行任务队列 invalidateJob(){} // 删除任务 queuePreFlushCb(){} // 添加前置回调函数并开始执行任务队列 queuePostFlushCb(){} // 添加后置回调函数并开始执行任务队列 flushPreFlushCbs(){} // 执行前置回调函数 flushPostFlushCbs(){} // 执行后置回调函数
我们首先要知道几个关键变量
let isFlushing = false // 是否正在清空任务队列 let isFlushPending = false // 清队任务已创建,等待清空状态 const queue: SchedulerJob[] = [] // 任务队列 let flushIndex = 0 // 当前正在执行的任务在任务队列中的索引
然后我们从
queueJob
这个函数开始/* 这个函数主要是将一个任务进行入队操作 然后在满足条件的情况下启动queueFlush */ export function queueJob(job: SchedulerJob) { /** * 任务可入队逻辑 * 1. 任务队列为空 * 2. 待入队任务不能存在于任务队列中(按情况分析) */ if ( (!queue.length || !queue.includes( job, /* 在正在清空队列且当前待入队任务是可以递归时, 说明当前任务一定和当前正在执行任务是同一任务,所以+1, 就是为了保证待入队任务和正在执行任务相同,但不能和后面待执行任务相同 */ isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex )) && job !== currentPreFlushParentJob ) { // 二分查找任务在队列中的位置 const pos = findInsertionIndex(job) if (pos > -1) { queue.splice(pos, 0, job) } else { queue.push(job) } queueFlush() } }
queueFlush
function queueFlush() { /** 清队任务创建后禁止再次创建更多的清队任务 因为在入队操作完成后,flushJobs会在一次递归中将任务队列全部清空,所以只需要一次清队任务即可 */ if (!isFlushing && !isFlushPending) { isFlushPending = true /* 清队任务创建成功,并记录下当前清队任务,这个标记可以用于nextTick创建自定义函数, 说明nextTick的执行时机是在清队任务后的,其实从这个地方就可以理解nextTick的执行原理了 */ currentFlushPromise = resolvedPromise.then(flushJobs) } }
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)) try { // 遍历任务队列 for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { const job = queue[flushIndex] if (job && job.active !== false) { if (__DEV__ && checkRecursiveUpdates(seen!, job)) { continue } // 执行当前任务 callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) } } } finally { // 重置当前任务索引 flushIndex = 0 // 清空任务队列 queue.length = 0 // 执行后置回调任务队列 flushPostFlushCbs(seen) // 重置清队任务的状态 isFlushing = false currentFlushPromise = null /* 因为清队任务执行期间也会有任务入队,所以为了清队执行完成 就需要判断各任务队列的长度,然后递归执行 */ if ( queue.length || pendingPreFlushCbs.length || pendingPostFlushCbs.length ) { flushJobs(seen) } } }
总结
- nextTick的执行时机是在任务队列(前置、主任务、后置)清除后的,
currentFlushPromise
是清队任务的promise标记 - 任务队列执行顺序:执行前置回调任务队列 -> 执行主任务队列 -> 执行后置回调任务队列