nextTick
是 Vue 3 中用于异步执行回调函数的函数,它会将回调函数延迟到下一个微任务队列中执行。其中,Vue 更新 DOM 是异步的。下面是对 nextTick
函数的详细解释:
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
}
let currentFlushPromise: Promise<void> | null = null
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
解释如下:
函数签名:
nextTick(this: T, fn?: (this: T) => R): Promise>
:这是 nextTick
函数的类型签名。它接受以下参数:
this
:不是参数,是ts中的一个语法,给 this 定义类型。给用于绑定回调函数中的 this
上下文,可以省略。fn
:要异步执行的回调函数,是一个函数,可以省略。Promise
,Promise
的泛型为 Awaited
,表示回调函数执行后的返回值。实现逻辑:
nextTick
函数首先定义了一个 Promise
对象 p
,这个 Promise
对象被初始化为 currentFlushPromise
或 resolvedPromise
,取决于当前是否处于刷新队列(flush
)的过程中。currentFlushPromise
是一个全局变量,用于表示当前正在刷新队列,如果不在刷新队列中,则使用 resolvedPromise
,它是一个已经 resolved(已解决)的 Promise
。
接着,函数检查是否传入了回调函数 fn
。如果传入了 fn
,则会返回一个 Promise.then()
,去执行一个微任务,这个 Promise
在微任务队列中执行回调函数(如果使用 await nextTick()
也相当于执行一个微任务)。如果没有传入 fn
,则直接返回 p
,这意味着如果没有回调函数,会返回一个已经 resolved 的 Promise
。
在返回 Promise
的过程中,函数会根据传入的 this
上下文(如果有的话),使用 fn.bind(this)
来绑定回调函数中的 this
,确保回调函数在执行时具有正确的上下文。
总结:
nextTick
函数用于将回调函数异步执行,将回调函数放入下一个微任务队列中。它的核心逻辑是使用 Promise
来管理回调函数的异步执行,也就是把我们的代码放到一个 Promise
中去执行微任务,把我们的代码变成异步的。resolved
的 Promise
。这个函数在 Vue 3 中用于在数据更新后执行回调函数,确保数据更新后的 DOM 操作在下一个微任务中执行,以提高性能和响应速度。Vue 更新 DOM 是异步的,然后我们看一下队列是怎么更新的。
// job 是组件实例上的 update 方法,生成 instance.update 函数
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
)
) {
// 如果任务的 id 为 null,直接将任务添加到队列末尾
if (job.id == null) {
queue.push(job)
} else {
// 如果任务的 id 不为 null,根据任务的 id 寻找插入位置
// 将任务插入到合适的位置,以保持任务按照 id 的顺序执行
queue.splice(findInsertionIndex(job.id), 0, job)
}
// 触发任务刷新,开始执行任务队列中的任务
queueFlush()
}
}
const queue: SchedulerJob[] = []
这段代码是 Vue 3 内部用于调度和执行任务的关键部分,主要涉及到任务队列的管理和触发任务执行的逻辑。
解释如下:
queueJob
函数用于将一个任务 job
添加到任务队列 queue
中,并触发任务刷新(queueFlush
)。
在添加任务之前,首先会进行一些条件判断:
queue.includes
方法检查当前任务 job
是否已经在任务队列中。这里的检查是为了避免任务的重复添加。如果任务队列中已经包含了当前任务,就不再添加。如果当前任务 job
通过了上述检查条件,就可以将它添加到任务队列中:
id
属性为 null
,表示没有指定任务的唯一标识,直接将任务添加到队列末尾(queue.push(job)
)。id
属性不为 null
,表示指定了任务的唯一标识,需要根据该标识找到合适的插入位置,以保持任务队列中的任务按照 id
的顺序执行(按照 job id 自增的顺序排列)。这时会调用 findInsertionIndex(job.id)
来找到插入位置,然后使用 splice
方法插入任务到指定位置。最后,调用 queueFlush
函数,触发任务刷新。任务刷新会开始执行任务队列中的任务。
总结:queueJob
函数用于添加任务到任务队列中,并确保任务不会重复添加。它也负责触发任务刷新,以开始执行任务队列中的任务。这是 Vue 3 中任务调度的核心逻辑之一,用于管理异步任务的执行顺序。
以上的 id
和 job
都存在于 getCurrentInstance
中,因此可以打印看一下:
import { reactive, ref, nextTick, getCurrentInstance, watch } from 'vue'
const instance = getCurrentInstance()
console.log(instance);
返回一个对象,源码里面的 id
就是这里的 uid
,里面的 job
函数就是这里的 update
函数。也就是说:const update = (instance.update = () => effect.run())
, update.id = instance.uid
。所以,源码就是获取了这两者,放入 queue 队列。
function queueFlush() {
if (!isFlushing && !isFlushPending) {
// 如果没有正在刷新任务队列(isFlushing 为 false)
// 且没有挂起的刷新请求(isFlushPending 为 false)
// 设置 isFlushPending 为 true,表示有一个刷新请求挂起
isFlushPending = true
// 使用 resolvedPromise.then(flushJobs) 来延迟执行 flushJobs 函数
// 这样可以让当前的 JavaScript 执行栈完成后再执行刷新操作
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
这段代码是 Vue 3 内部用于触发任务刷新(flushJobs
)的逻辑。它确保在没有正在刷新任务队列(isFlushing
为 false
)且没有挂起的刷新请求(isFlushPending
为 false
)时才会触发任务刷新。
解释如下:
queueFlush
函数用于触发任务刷新,但它不会立即执行刷新操作,而是通过 Promise 的方式进行延迟执行。
在执行触发刷新前,首先进行两个条件的检查:
isFlushing
为 false
:表示没有正在刷新任务队列,确保不会同时执行多次刷新。isFlushPending
为 false
:表示没有挂起的刷新请求,确保不会重复触发刷新。如果上述两个条件都满足,就会执行以下操作:
isFlushPending
设置为 true
,表示有一个刷新请求挂起。currentFlushPromise
,使用 resolvedPromise
来生成一个已经 resolved(已完成)状态的 Promise,并通过 .then
方法将 flushJobs
函数作为回调传递进去。这样,flushJobs
函数会在下一个微任务中执行。当 currentFlushPromise
这个 Promise 被 resolved 时,会触发 flushJobs
函数的执行,从而实际执行任务刷新的操作。
总结:queueFlush
函数用于触发任务刷新,但会确保在没有正在刷新任务队列的情况下,且没有挂起的刷新请求时才触发。核心就是通过将刷新操作包装成 Promise,把 flushJobs
放入下一个微任务中执行,以确保任务刷新是异步的,不会阻塞当前 JavaScript 执行栈。这种设计有助于保持 Vue 3 的响应性和性能。
function flushJobs(seen?: CountMap) {
// 标志当前没有挂起的刷新请求
isFlushPending = false
// 标志当前正在执行刷新操作
isFlushing = true
if (__DEV__) {
// 如果是开发环境,创建一个用于追踪任务数量的 Map 对象
seen = seen || new Map()
}
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child.
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped.
// 在执行刷新前,对任务队列进行排序,以确保以下两点:
// 1. 组件更新的顺序是从父组件到子组件的(因为父组件总是在子组件之前创建,所以其渲染效果具有较小的优先级编号)。
// 2. 如果在父组件的更新期间卸载了子组件,可以跳过子组件的更新。
queue.sort(comparator)
// 条件使用 checkRecursiveUpdate 必须在 try...catch 块外部决定
// 因为 Rollup 默认会在 try...catch 块内部取消树摇,这可能导致所有警告代码都无法被摇掉。
// 尽管它们最终会被像 terser 这样的压缩器摇掉,但某些压缩器可能无法做到这一点(例如 https://github.com/evanw/esbuild/issues/1610)。
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 包装,以捕获可能的错误
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
// 清空 flushIndex,重置任务队列
flushIndex = 0
queue.length = 0
// 执行在刷新后的回调函数
flushPostFlushCbs(seen)
// 标志当前刷新结束
isFlushing = false
// 将当前刷新的 Promise 置为 null
currentFlushPromise = null
// 如果任务队列中仍有任务或者有挂起的 postFlush 回调
// 继续执行刷新操作,直到任务队列为空
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
这段代码是 Vue 3 中用于执行任务刷新的关键逻辑,它会在 queueFlush
函数触发时执行。
解释如下:
flushJobs
函数用于执行任务刷新,它在 queueFlush
函数触发后被调用。
首先,它会将 isFlushPending
置为 false
,表示当前没有挂起的刷新请求,然后将 isFlushing
置为 true
,表示当前正在执行刷新操作。
在开发环境中,创建一个用于追踪任务数量的 seen
Map 对象。
对任务队列 queue
进行排序,确保组件的更新按从父到子的顺序执行,同时也考虑了组件在父组件更新期间被卸载的情况。
使用 check
函数来检查是否存在递归更新的情况。这个检查在开发环境下才会执行。
进入循环,依次处理任务队列 queue
中的任务。如果任务是激活状态且没有递归更新的情况,就执行该任务。callWithErrorHandling
函数用于包装任务的执行,以捕获可能的错误,其实就是包装了 job 函数,并且会把错误抛出。
finally
块中,清空 flushIndex
,重置任务队列 queue
,确保任务只会执行一次。
执行在刷新后的回调函数 flushPostFlushCbs
,这些回调函数通常用于清理或执行一些不同的调度政策。
最后,将 isFlushing
置为 false
,表示刷新操作结束,将当前刷新的 Promise currentFlushPromise
置为 null
。
如果任务队列 queue
中仍有任务,或者有挂起的 postFlush
回调函数,就继续执行刷新操作,直到任务队列为空。
总结:flushJobs
函数是 Vue 3 中执行任务刷新的核心逻辑,它会在 queueFlush
触发后执行,负责遍历并执行任务队列中的任务,确保组件更新按正确的顺序执行,并处理递归更新的情况。这个函数的设计是为了保持 Vue 3 的响应性和性能。
比如我们可以在父组件中嵌套一个子组件,让父组件和子组件都输出 getCurrentInstance
(上面提到过~)。可以看到 uid 的顺序就是先创建父组件,再创建子组件。
至于上面说的 flushPostFlushCbs
函数,其实我们在学watch
的时候已经接触过了。
watch((xxx)=> {
},{
flush: 'post'
})
这里的 flush: 'post'
其实就是调用了flushPostFlushCbs
函数。