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 标记 - 工作队列执行程序:执行前置回调工作队列 -> 执行主工作队列 -> 执行后置回调工作队列