阅读此章前假设已经理解 JavaScript 的 事件循环机制
用法:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
同步回调函数方式实现:
// 修改数据vm.msg = 'Hello world!';// DOM 还没更新Vue.nextTick(function () {// DOM 更新了});
同步 Promise 方式实现:
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)Vue.nextTick().then(function () {// DOM 更新了});
尽管 MVVM 框架并不推荐直接操作 DOM,但有时候确实有这样的需求,尤其是和第三方插件进行配合的时候,免不了要进行 DOM 操作。而 nextTick
就提供了一个桥梁,确保我们操作的更新后的 DOM。
它跟全局方法 Vue.nextTick
一样,不同的是回调的 this
自动绑定到调用它的实例上。
new Vue({// ...methods: {// ...example: function () {// 修改数据this.message = 'changed';// DOM 还没有更新this.$nextTick(function () {// DOM 现在更新了// `this` 绑定到当前实例this.doSomethingElse();});},},});
import { createApp, nextTick } from 'vue';const app = createApp({setup() {const message = ref('Hello!');const changeMessage = async (newMessage) => {message.value = newMessage;await nextTick();console.log('Now DOM is updated');};},});
createApp({// ...methods: {// ...example() {// 修改数据this.message = 'changed';// DOM 尚未更新this.$nextTick(function () {// DOM 现在更新了// `this` 被绑定到当前实例this.doSomethingElse();});},},});
从字面意思理解,next
下一个,tick
滴答(钟表)来源于定时器的周期性中断(输出脉冲),一次中断表示一个 tick
,也被称作 时钟滴答,nextTick
顾名思义就是下一个时钟滴答。
if (typeof Promise !== 'undefined' && isNative(Promise)) {const p = Promise.resolve();timerFunc = () => {p.then(flushCallbacks);// In problematic UIWebViews, Promise.then doesn't completely break, but// it can get stuck in a weird state where callbacks are pushed into the// microtask queue but the queue isn't being flushed, until the browser// needs to do some other work, e.g. handle a timer. Therefore we can// "force" the microtask queue to be flushed by adding an empty timer.if (isIOS) setTimeout(noop);};isUsingMicroTask = true;}
源码分析:
flushCallbacks
函数。Vue 如何检测到 DOM 更新完毕呢?
nextTick 通过访问 Promise.then
和 MutationObserver
可以访问的微任务队列。
MutationObserver 是 HTML5 新增的内置对象,用于监听 DOM 修改事件,能够监听到节点的属性、文本内容、子节点等的改动。
else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||// PhantomJS and iOS 7.xMutationObserver.toString() === '[object MutationObserverConstructor]')) {// Use MutationObserver where native Promise is not available,// e.g. PhantomJS, iOS7, Android 4.4// (#6466 MutationObserver is unreliable in IE11)let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true}
源码分析:
从源码可以看出,如果运行环境(浏览器)支持 MutationObserver,则创建一个文本节点,监听这个文本节点的改动事件,以此来触发 nextTickHandler
(也就是 DOM 更新完毕的回调)的执行。后续会执行手工修改文本节点属性,这样就能进入到回调函数。
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {// Fallback to setImmediate.// Technically it leverages the (macro) task queue,// but it is still a better choice than setTimeout.timerFunc = () => {setImmediate(flushCallbacks)}}
setImmediate
是否存在,setImmediate
是高版本 IE (IE10+) 和 Edge 才支持的。flushCallbacks
执行 setImmediate
。else {// Fallback to setTimeout.timerFunc = () => {setTimeout(flushCallbacks, 0)}}
setTimeout
执行 flushCallbacks
。我们可以发现,给 timeFunc
赋值是一个降级的过程,这是因为 Vue 在执行的过程中,执行环境不同,所以要适配环境。
我们知道微任务队列和宏任务队列是交替执行的,在执行微任务的过程中后加入的队列的微任务,也会在下一次事件循环之前被执行。也就是说,宏任务总要等到微任务都执行完后才能执行,微任务有着更高的优先级。
队列控制的最佳选择是微任务,而微任务的最佳选择是 Promise。但是如果当前环境不支持 Promise,Vue 就不得不降级为宏任务来做队列控制了。
Vue2.5+ 降级方案(由上至下):
在 Vue3.x 中已经 nextTick
放弃兼容,直接使用 Promise.resolve().then()
。
function flushCallbacks() {pending = false;const copies = callback.slice(0);callbacks.length = 9;for (let i = 0; i < copies.length; i++) {copies[i]();}}
循环遍历,按照 队列 数据结构 "先进先出" 的原则,逐一执行所有 callback
。