Skip to main content

nextTick 的使用及其原理

一、nextTick 定义及用法

在 Vue 中更改响应式状态时,DOM 并不是同步更新的,而是由 Vue 将它们缓存在一个异步更新队列中,等队列中所有数据变化完成后,再统一进行 DOM 的更新,这是为了确保每个组件无论发生多少次状态改变,都只执行一次更新。

如果需要在响应式状态更改后立即访问更新后的 DOM,就需要用到 nextTick()

举个例子:

App.vue
<template>
<input ref="inputRef" :value="msg" />
<button @click="handleAdd">+1</button>
</template>

<script setup>
import { ref, nextTick } from 'vue';

const inputRef = ref();
const msg = ref(0);

const handleAdd = () => {
msg.value += 1;
// DOM 更新前
console.log(inputRef.value.value, 'update before');

nextTick(() => {
// DOM 更新后
console.log(inputRef.value.value, 'update after');
});
};
</script>

实现效果:

从上面例子可以看出,没有使用 nextTick 前,响应式状态更改后 DOM 并没有同步更新。

二、nextTick 原理

nextTick 源码位于 vue/src/core/util/next-tick.ts

1、初始变量

  • isUsingMicroTask:是否使用微任务
  • callbacks:回调函数
  • pending:是否正在处理
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks: Array<Function> = []
let pending = false

// ...

2、nextTick 方法

  1. 定义一个 _resolve 的标识用来判断回调是否结束;
  2. 使用 callbacks 收集传入的回调函数,并对其做了异常处理;
  3. 执行异步延迟函数 timerFunc;
  4. 如果没有传回调函数并且当前环境支持 Promise,返回一个 Promise 化的调用
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
// 定义一个 _resolve 用来判断回调是否结束
let _resolve

// 使用 callbacks 收集传入的回调函数,并对其做了异常处理
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})

// 执行异步延迟函数 timerFunc
if (!pending) {
pending = true
timerFunc()
}

// 如果没有传回调函数且当前环境支持 Promise,返回一个 Promise 化的调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}

3、timerFunc 方法

上面 nextTick 中用到的 timerFunc 方法,用于判断当前环境是否原生支持 Promise,支持则优先用 Promise,不支持则看是否支持 MutationObserver,不支持 MutationObserver 则看是否支持 setImmediate,以上都不支持则用 setTimeout。降级流程:

Promise > MutationObserver > SetImmediate > SetTimeout

在使用 PromiseMutationObserverisUsingMicroTask 会标记为 true,代表使用微任务来做异步流程控制。

timerFunc 的作用只是为了执行回调函数,在使用不同方法时加入一些特定的处理。比如使用 MutationObserver 时,会通过监听一个文本节点的内容变化来触发清空操作,充分利用了微任务的高优先级特性。

说明

在 2.5 版本的 vue 里,nextTick 为了兼容性考虑,使用宏任务 + 微任务的方案来处理,相关的 issue 很多,2.6 版本才统一优先使用微任务

总之,nextTick 会优先使用微任务(PromiseMutationObserver),不支持相应方法采用宏任务(setImmediatesetTimeout)代替。

let timerFunc
/**
* 虽然 MutationObserver 支持度比 Promise 更高
* 但在(iOS >= 9.3.3)中,触发 touch 事件时会有严重 bug,多次触发甚至会导致无响应
* 所以优先使用 Promise
*/
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 判断 1:是否支持 Promise <微任务>
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// 部分 IOS 中微任务队列不会及时清空,所以手动加个计时器清一下
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// 判断 2:是否支持 MutationObserver <微任务>,但它在 IE11 中不可靠
let counter = 1
const observer = new MutationObserver(flushCallbacks)
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)) {
// 判断 3:是否支持 setImmediate <宏任务>
// 虽然是宏任务,但效率优于 setTimeout
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 判断 4:以上都不支持则用 setTimeout <宏任务>
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

4、flushCallbacks 方法

上面 timerFunc 中无论是微任务还是宏任务,都会放到 flushCallbacks 中使用

flushCallbacks 是清空回调函数队列的方法,它会执行 callbacks 数组中的所有回调并清空它。

function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

三、总结

Vue 中 DOM 更新是异步的,Vue 将它们缓存在一个异步更新队列中,等队列中所有数据变化完成后,再统一进行 DOM 的更新,这是为了确保每个组件无论发生多少次状态改变,都只执行一次更新。如果要在状态改变后立即访问更新后的 DOM,就需要用到 nextTick()

nextTick() 会把回调函数放入 callbacks 等待执行,将执行函数放到微任务或者宏任务中,优先使用微任务 Promise,都不兼容才使用宏任务 setTimeout 来控制,最后执行函数依次执行 callbacks 中的回调并清空。