nextTick 的使用及其原理
一、nextTick 定义及用法
在 Vue 中更改响应式状态时,DOM 并不是同步更新的,而是由 Vue 将它们缓存在一个异步更新队列中,等队列中所有数据变化完成后,再统一进行 DOM 的更新,这是为了确保每个组件无论发生多少次状态改变,都只执行一次更新。
如果需要在响应式状态更改后立即访问更新后的 DOM,就需要用到 nextTick()。
举个例子:
- 一般用法
- 结合 async / await 使用
<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>
<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 = async () => {
msg.value += 1;
// DOM 更新前
console.log(inputRef.value.value, 'update before');
await 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 方法
- 定义一个
_resolve
的标识用来判断回调是否结束; - 使用
callbacks
收集传入的回调函数,并对其做了异常处理; - 执行异步延迟函数
timerFunc
; - 如果没有传回调函数并且当前环境支持 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
在使用 Promise 和 MutationObserver 时 isUsingMicroTask
会标记为 true
,代表使用微任务来做异步流程控制。
timerFunc 的作用只是为了执行回调函数,在使用不同方法时加入一些特定的处理。比如使用 MutationObserver 时,会通过监听一个文本节点的内容变化来触发清空操作,充分利用了微任务的高优先级特性。
在 2.5 版本的 vue 里,nextTick 为了兼容性考虑,使用宏任务 + 微任务的方案来处理,相关的 issue 很多,2.6 版本才统一优先使用微任务。
总之,nextTick 会优先使用微任务(Promise、MutationObserver),不支持相应方法采用宏任务(setImmediate、setTimeout)代替。
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 中的回调并清空。