基本语法
nextTick(callback);
基础使用
Vue 2 中使用 nextTick
基本用法:
<template>
<div>
<button @click="updateData">更新数据</button>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: '初始值'
};
},
methods: {
updateData() {
this.message = '更新后的值';
// 使用 $nextTick 确保 DOM 更新
this.$nextTick(() => {
console.log('DOM 已更新,当前 message 内容:', this.message);
});
}
}
};
</script>
返回 Promise:
this.$nextTick()
.then(() => {
console.log('DOM 已更新,当前 message 内容:', this.message);
});
Vue 3 中使用 nextTick
在 Vue 3 中,nextTick 依然存在,但由于响应式系统的改进,使用上更加灵活。
基本用法:
<template>
<div>
<button @click="updateData">更新数据</button>
<p>{{ message }}</p>
</div>
</template>
<script>
import { nextTick } from 'vue';
export default {
data() {
return {
message: '初始值'
};
},
methods: {
updateData() {
this.message = '更新后的值';
// 使用 nextTick 确保 DOM 更新
nextTick(() => {
console.log('DOM 已更新,当前 message 内容:', this.message);
});
}
}
};
</script>
返回 Promise:
nextTick()
.then(() => {
console.log('DOM 已更新,当前 message 内容:', this.message);
});
作用
在 Vue 2 中,nextTick 方法用于在下次 DOM 更新循环结束之后执行延迟回调。这通常用于在响应式数据更改后,确保 DOM 已经更新。
在 Vue 3 中,nextTick 的实现主要是为了确保在 DOM 更新后执行特定的回调。这是通过 scheduler 和 flushCallbacks 等机制来调度和执行回调的。
vue2源码体现
Vue 2 中的 nextTick 实现原理主要基于 JavaScript 的异步编程模型,利用微任务(Microtasks)来确保在 DOM 更新后执行一些操作。
1. 微任务与宏任务
- 微任务:微任务是在当前任务完成后、下一次事件循环执行之前执行的任务。它通常使用
Promise、MutationObserver或process.nextTick(在 Node.js 中)等机制实现。 - 宏任务:例如
setTimeout、setInterval等,它们的执行在微任务之后。
Vue 2 的 nextTick 主要依赖于微任务的特性,以确保 DOM 更新在回调执行前完成。
2. 代码实现
在 Vue 2 的 nextTick 实现中,基本步骤如下:
- 回调队列:定义一个数组
callbacks用于存储待执行的回调函数。当用户调用nextTick时,将提供的回调函数添加到这个数组中。 - 执行机制:使用一个定时器函数
timerFunc来处理回调。这个函数决定了如何执行这些回调,通常会选择最佳的执行机制(如Promise.then或MutationObserver)。 - 回调执行:当
nextTick被调用,若没有 pending(标识是否有待执行的任务),则将 pending 设置为true,并启动定时器函数。在定时器函数中,所有存储的回调函数将被逐一执行。 - 错误处理:在执行回调时,vue 还会包裹
try-catch以捕获可能的错误,确保一个回调的失败不会影响其他回调的执行。
3. 实现流程
以下是 Vue 2 nextTick 的具体实现流程:
-
用户调用
nextTick:将传入的回调函数压入callbacks数组中。 -
检查 pending:如果
pending为false,则表明没有正在执行的回调,接下来将其设置为true,并调用timerFunc。 -
定时器触发:
- 在定时器函数中,使用适当的方法(如
Promise或MutationObserver)将flushCallbacks离子执行加入微任务队列。 flushCallbacks函数负责清空callbacks数组,并执行所有存储的回调。
- 在定时器函数中,使用适当的方法(如
-
回调执行:依次调用
callbacks中的每个函数,并在调用过程中进行错误处理。
// 源码位置 src\core\util\next-tick.ts
import { noop } from 'shared/util' // 导入一个空函数 noop,用于无操作
import { handleError } from './error' // 导入一个用于处理错误的函数
import { isIE, isIOS, isNative } from './env' // 导入环境相关的检查函数
export let isUsingMicroTask = false // 标识当前是否使用微任务
const callbacks: Array<Function> = [] // 用于存储待执行的回调函数
let pending = false // 标识当前是否有待执行的任务
// 刷新并执行所有回调函数
function flushCallbacks() {
pending = false // 设置 pending 为 false,表示任务已经完成
const copies = callbacks.slice(0) // 复制当前的回调数组,以避免修改影响到执行
callbacks.length = 0 // 清空原数组
for (let i = 0; i < copies.length; i++) {
copies[i]() // 执行每个回调
}
}
// 选择微任务的定时器函数
let timerFunc
// 根据环境选择合适的微任务定时器
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 如果支持 Promise,使用 Promise.then
const p = Promise.resolve() // 创建一个立即解决的 Promise
timerFunc = () => {
p.then(flushCallbacks) // 在 Promise 解析时执行 flushCallbacks
if (isIOS) setTimeout(noop) // 针对 iOS 的某些问题强制刷新微任务
}
isUsingMicroTask = true // 标识使用微任务
} else if (
!isIE && // 不是 IE 浏览器
typeof MutationObserver !== 'undefined' && // 支持 MutationObserver
(isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// 使用 MutationObserver
let counter = 1
const observer = new MutationObserver(flushCallbacks) // 创建 MutationObserver 观察器
const textNode = document.createTextNode(String(counter)) // 创建文本节点
observer.observe(textNode, {
characterData: true // 观察字符数据的变化
})
timerFunc = () => {
counter = (counter + 1) % 2 // 触发字符数据的变化
textNode.data = String(counter) // 切换文本数据,通知观察者刷新回调
}
isUsingMicroTask = true // 标识使用微任务
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 如果支持 setImmediate,作为后备方案
timerFunc = () => {
setImmediate(flushCallbacks) // 使用 setImmediate 刷新回调
}
} else {
// 如果以上都不支持,使用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0) // 使用 setTimeout 刷新回调
}
}
// 定义 nextTick 函数的重载
export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
/**
* @internal
*/
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx) // 在线程中调用回调函数
} catch (e: any) {
handleError(e, ctx, 'nextTick') // 处理错误
}
} else if (_resolve) {
_resolve(ctx) // 如果没有回调但需要解析 Promise,调用 resolve
}
})
if (!pending) {
pending = true // 标记为正在处理
timerFunc() // 开始执行定时器
}
// 如果没有回调并且支持 Promise,返回一个新的 Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve // 保存解析函数
})
}
}
vue3中源码体现
Vue 3 中的 nextTick 原理主要基于任务调度和异步数据更新机制,旨在确保在 DOM 更新后执行回调函数。其核心原理可以总结为以下几点:
1. 微任务队列
- Vue 使用微任务(Microtask)来实现异步执行。在执行 Vue 的响应式更新时,更新将被推入一个任务队列,使用
Promise.resolve().then()来确保这些任务在 DOM 更新后执行。 - 这种机制的好处是可以将 DOM 更新和回调函数的执行分开,确保所有数据变更得到处理后,再进行下一步操作。
2. 任务合并
- Vue 中的调度器将多个任务合并为一个更新批次,从而减少了对 DOM 的多次操作,提高性能。
- 当数据变化时,会把任务加入一个队列,待所有任务处理完后,再统一执行相关的回调。
3. 递归处理
- 为了防止由于任务重复触发导致的无限递归,Vue 在任务执行时会检查执行次数,防止超过设定的最大递归限制(如 100 次)。
- 在执行某个任务时,如果该任务又引发了自己的重复执行,Vue 会记录并限制该任务的执行,以免造成栈溢出。
4. 状态标志
- 每个任务都被标记(使用标志位),如
QUEUED(已入队)、ALLOW_RECURSE(允许递归)等,以便管理任务的状态和执行条件。 - 通过这些标志位,调度器可以灵活控制任务的执行顺序和执行条件。
5. 前置和后置回调
- Vue 的调度器还支持前置和后置回调,允许在更新操作的特定阶段执行自定义逻辑。
- 前置回调会在真正更新前被执行,而后置回调则是在更新后被执行,这样可以满足不同场景中的需要。
// 源码位置 packages\runtime-core\src\scheduler.ts
export enum SchedulerJobFlags {
QUEUED = 1 << 0, // 表示任务已被加入队列
PRE = 1 << 1, // 表示前置任务
ALLOW_RECURSE = 1 << 2, // 允许递归调用
DISPOSED = 1 << 3, // 表示任务已被处置
}
// 定义调度任务的接口
export interface SchedulerJob extends Function {
id?: number // 唯一标识符
flags?: SchedulerJobFlags // 任务状态标志
i?: ComponentInternalInstance // 组件实例信息
}
const queue: SchedulerJob[] = [] // 任务队列
let flushIndex = -1 // 当前刷新的索引
const pendingPostFlushCbs: SchedulerJob[] = [] // 待处理的后置回调
let activePostFlushCbs: SchedulerJob[] | null = null // 当前活动的后置回调队列
let postFlushIndex = 0 // 后置回调索引
const resolvedPromise = Promise.resolve() as Promise<any> // 已解决的 Promise
let currentFlushPromise: Promise<void> | null = null // 当前刷新 Promise
const RECURSION_LIMIT = 100 // 最大递归限制
type CountMap = Map<SchedulerJob, number> // 用于记录递归调用次数的映射
// nextTick 方法,用于在 DOM 更新后执行回调
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
}
// 查找合适的插入位置,以保持任务队列的顺序
function findInsertionIndex(id: number) {
let start = flushIndex + 1
let end = queue.length
while (start < end) {
const middle = (start + end) >>> 1
const middleJob = queue[middle]
const middleJobId = getId(middleJob)
if (
middleJobId < id ||
(middleJobId === id && middleJob.flags! & SchedulerJobFlags.PRE)
) {
start = middle + 1
} else {
end = middle
}
}
return start
}
// 将任务加入队列
export function queueJob(job: SchedulerJob): void {
if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
const jobId = getId(job)
const lastJob = queue[queue.length - 1]
if (
!lastJob ||
(!(job.flags! & SchedulerJobFlags.PRE) && jobId >= getId(lastJob))
) {
queue.push(job) // 快速路径
} else {
queue.splice(findInsertionIndex(jobId), 0, job) // 按顺序插入
}
job.flags! |= SchedulerJobFlags.QUEUED // 标记为已加入队列
queueFlush() // 刷新队列
}
}
// 控制队列的刷新
function queueFlush() {
if (!currentFlushPromise) {
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
// 加入后置回调
export function queuePostFlushCb(cb: SchedulerJobs): void {
if (!isArray(cb)) {
if (activePostFlushCbs && cb.id === -1) {
activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
} else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
pendingPostFlushCbs.push(cb)
cb.flags! |= SchedulerJobFlags.QUEUED
}
} else {
pendingPostFlushCbs.push(...cb)
}
queueFlush()
}
// 刷新所有前置回调
export function flushPreFlushCbs(
instance?: ComponentInternalInstance,
seen?: CountMap,
i: number = flushIndex + 1,
): void {
if (__DEV__) {
seen = seen || new Map()
}
for (; i < queue.length; i++) {
const cb = queue[i]
if (cb && cb.flags! & SchedulerJobFlags.PRE) {
if (instance && cb.id !== instance.uid) {
continue
}
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
continue
}
queue.splice(i, 1)
i--
if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
cb()
if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
}
}
}
// 刷新所有后置回调
export function flushPostFlushCbs(seen?: CountMap): void {
if (pendingPostFlushCbs.length) {
const deduped = [...new Set(pendingPostFlushCbs)].sort(
(a, b) => getId(a) - getId(b),
)
pendingPostFlushCbs.length = 0
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.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) cb()
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
activePostFlushCbs = null
postFlushIndex = 0
}
}
// 获取任务 ID
const getId = (job: SchedulerJob): number =>
job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
// 执行所有任务
function flushJobs(seen?: CountMap) {
if (__DEV__) {
seen = seen || new Map()
}
const check = __DEV__ ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job) : NOOP
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && !(job.flags! & SchedulerJobFlags.DISPOSED)) {
if (__DEV__ && check(job)) {
continue
}
if (job.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
job.flags! &= ~SchedulerJobFlags.QUEUED
}
callWithErrorHandling(job, job.i, job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER)
if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
job.flags! &= ~SchedulerJobFlags.QUEUED
}
}
}
} finally {
for (; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job) {
job.flags! &= ~SchedulerJobFlags.QUEUED
}
}
flushIndex = -1
queue.length = 0
flushPostFlushCbs(seen)
currentFlushPromise = null
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}
// 检查是否出现递归更新
function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
const count = seen.get(fn) || 0
if (count > RECURSION_LIMIT) {
const instance = fn.i
const componentName = instance && getComponentName(instance.type)
handleError(
`Maximum recursive updates exceeded${
componentName ? ` in component <${componentName}>` : ``
}. ` +
`This means you have a reactive effect that is mutating its own ` +
`dependencies and thus recursively triggering itself. Possible sources ` +
`include component template, render function, updated hook or ` +
`watcher source function.`,
null,
ErrorCodes.APP_ERROR_HANDLER,
)
return true
}
seen.set(fn, count + 1)
return false
}
vue2 和 vue3实现nextTick 的差异
1. 内部实现机制
-
Vue 2:
- 使用微任务队列管理异步回调。Vue 2 在实现中会依赖
Promise、MutationObserver,并在没有这两者时回退到setTimeout。 nextTick是通过一个默认的回调队列来执行,并确保在 DOM 更新后执行用户提供的回调。
- 使用微任务队列管理异步回调。Vue 2 在实现中会依赖
-
Vue 3:
- 采用了更简化的实现策略,依然使用微任务,但引入了更强大的响应式系统和更高效的调度机制。
- Vue 3 的
nextTick可以直接使用Promise.resolve()作为主要的微任务实现,简化了代码。
2. API 的一致性与返回值
-
Vue 2:
- 支持通过回调函数或返回 Promise 的方式来使用
nextTick。 - 使用
nextTick时可以提供一个回调函数,也可以不提供,若不提供则会返回一个 Promise。
- 支持通过回调函数或返回 Promise 的方式来使用
-
Vue 3:
- 在 API 方面更为一致,
nextTick的行为改善了,保持了相同的使用方式,但返回值对 Promise 的处理更加明确。 - 直接返回 Promise,允许使用
async/await的语法,使得在处理副作用时更为自然。
- 在 API 方面更为一致,
3. 性能优化
-
Vue 2:
- 因为使用了多种方法从回调队列中提取微任务,并且在多次调用
nextTick时,可能导致响应时间的延迟。
- 因为使用了多种方法从回调队列中提取微任务,并且在多次调用
-
Vue 3:
- 通过重构响应式系统和调度机制,获得了更高效的更新策略。Vue 3 的
nextTick性能层面得到了优化,可以更快地处理多个异步任务。
- 通过重构响应式系统和调度机制,获得了更高效的更新策略。Vue 3 的
4. 新的响应式架构
- Vue 3 中引入了全新的响应式 API(基于 Proxy),这影响了
nextTick的应用场景。在 Vue 3 中,组件的响应式更新更加高效,因此在使用nextTick时可以更好地控制更新时机,和提高整体性能。