一文搞懂 Vue 的异步更新机制以及$nextTick 原理
1.JS 事件循环和微/宏任务理解
由于 JavaScript 是单线程的,这就决定了它的任务不可能只有同步任务,那些耗时很长的任务如果也按同步任务执行的话将会导致页面阻塞,所以 JavaScript 任务一般分为两类:同步任务与异步任务,而异步任务又分为宏任务与微任务。
1、宏任务: script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI rendering
注意:使用
setImmediate
可以在当前事件循环的末尾,立即触发一个回调函数,而不需要指定延迟时间。这对于需要尽快执行的异步任务很有用,且优先级高于setTimeout
。
以下是一个示例演示如何使用 setImmediate
:
console.log("Start");
setImmediate(() => {
console.log("This is setImmediate callback");
});
setTimeout(() => {
console.log("This is setTimeout callback");
}, 0);
console.log("End");
在上面的例子中,Start
和 End
都会先输出,因为它们是在当前事件循环的第一个和最后一个阶段执行的。然后,setImmediate
的回调函数会立即触发,输出 This is setImmediate callback
。最后,setTimeout
的回调函数会在下一个事件循环的 定时器
阶段触发,输出 This is setTimeout callback
。
2、微任务: promise.then、MutationObserver、Object.observe、process.nextTick(nodejs)
基本执行顺序
第一次宏任务完成 → 页面渲染 → 第一次宏任务完成(包含上一次宏任务事件时,异步队列中加入的宏任务) → 页面渲染…
宏任务(macrotask)
宏任务是进入任务栈等待主线程执行的主代码块,包括从异步队列里加入到栈的,如 setTimeout()、setInterval()的回调,其中不含异步队列中的微任务如 Promise.then 回调。
此时注意事件循环中(event loop)从异步队列加入到栈的宏任务是作为下一个事件来执行的,由于 GUI 渲染线程机制,每次事件循环后都会进行页面渲染,如下:
第一次宏任务完成 → 页面渲染 → 第一次宏任务完成(包含上一次宏任务事件时,异步队列中加入的宏任务如 setTimeout 等) → 页面渲染…
常见宏任务有:
主代码块(实际上是同步任务,所以说同步任务本质上就是宏任务)
setTimeout
setInterval
微任务(microtask)
微任务是异步队列中,在当前这一次宏任务执行完后,页面渲染之前要执行的任务。
此时注意,即使当前微任务执行过程中,产生了新的微任务,也会在下一个宏任务开始执行之前且当前事件循环结束之前执行完所有的微任务。——> 所以微任务一定先于宏任务
第一次宏任务 → 第一次所有微任务 → 页面渲染 → 第二次宏任务(包含上一次宏任务事件时,异步队列中加入的宏任务)→ 第二次所有微任务 → 页面渲染…
常见微任务有:
process.nextTick()
Promise
Object.observe
执行顺序总结
理解:同步任务(异步任务进入异步队列)——> (同步任务执行完毕)微任务(真正更新 data) ——> 更新 DOM(在一次事件循环的最末尾) ——> (下一次事件循环开启)宏任务和同步任务(同步任务本身就是宏任务)
1.主体代码(第一次事件循环开始,所有的 script 代码)作为宏任务进入任务执行栈,但在主线程执行之前要做一系列操作判断。
2.判断当前任务是同步还是异步,同步的由主线程在任务栈中按先进后出顺序(先局部上下文,再全局上下文)执行,异步判断是宏任务还是微任务。
3.异步中的宏任务放入异步的宏任务 event Table(异步队列分两种,宏任务队列和微任务队列,event Table 也一样),微任务进入微任务 event Table,在回调函数注册之后,再次进入它们对应的队列。
4.当主线程的任务执行完后,会检查微任务队列是否有任务,如果有就执行,如此循环,知道微任务队列没有任务。
5.当前事件的微任务执行完后,开始执行下一次事件,即会执行宏任务队列中的宏任务,如此循环下去,直到没有任务。
如果不理解,简单讲比如:A 是当前页面 script 代码第一次执行事件,B 是第二次事件
a.顺序执行同步任务
b.遇到异步任务
c.遇到异步任务:
异步微任务,插入当前事件 A 队尾等待执行
异步宏任务,插入下次事件循环 B 排队执行
2.基本概念
Vue 的异步更新机制
Vue 采用了基于虚拟 DOM 的异步更新机制,将数据变更和 DOM 更新解耦,以提高性能和渲染效率。下面我们来简单了解一下 Vue 的异步更新机制的工作原理。
响应式原理
Vue 通过响应式数据机制实现了数据的自动追踪和更新。当我们修改 Vue 实例的响应式数据时,Vue 会自动跟踪数据的变化,并在需要的时候触发视图的更新。
虚拟 DOM 和渲染过程
在 Vue 中,数据变更并不直接更新实际的 DOM 元素,而是通过虚拟 DOM 进行中间层的处理。虚拟 DOM 是一个轻量级的 JavaScript 对象,它以树形结构表示整个 DOM 结构。
当数据发生变化时,Vue 会重新渲染虚拟 DOM 树。Vue 会比较新旧虚拟 DOM 的差异,找出需要更新的部分,并将差异应用于实际的 DOM 元素,最终实现 DOM 的更新。
异步更新的优势
Vue 的异步更新机制带来了以下几个优势:
- 性能优化:将多个数据变更合并成一次更新,避免频繁的 DOM 操作,提高渲染效率。
- 批量更新:将数据变更放入一个队列中,只在下一个事件循环中进行更新,避免同步更新可能引发的性能问题和 UI 闪烁现象。
3.vue 的异步更新
Vue 是异步更新的!(指的是 DOM 更新)
Data 对象:vue 中的 data 方法中返回的对象;
Dep 对象:每一个 Data 属性都会创建一个 Dep,用来搜集所有使用到这个 Data 的 Watcher 对象;
Watcher 对象:组件视图对象,主要用于渲染 DOM。当一个 Vue 实例被创建时,Vue 会自动在其内部创建一些Watcher
对象。**这些Watcher
对象会与模板中使用的数据属性建立关联。**当这些数据属性发生变化时,关联的Watcher
对象会收到通知,并执行相应的回调函数,从而更新视图。
1.Vue 中的 DOM 更新是异步的
<template>
<div class="next_tick">
<div ref="title" class="title">{{ name }}</div>
</div>
</template>
<script>
export default {
data() {
return {
name: "前端南玖",
};
},
mounted() {
this.name = "front end";
console.log("sync", this.$refs.title.innerText);
this.$nextTick(() => {
console.log("nextTick", this.$refs.title.innerText);
});
},
};
</script>
从上面的效果我们可以发现,Vue 中的 DOM 更新是异步的,我们在修改完 Data 之后并不能立刻获取修改后的 DOM 元素。
那么我们什么时候才能获取到真正的 DOM 元素?
在 Vue 的 this.$nextTick 回调中。
Vue 中异步更新的流程和底层:
理解:一次 vue 更新的大概流程是,修改 data 数据 ——> 构建 data 数据的 watcher 队列 ——> 更新 dom(执行 update 钩子,根据 watcher 队列更新 dom)——> 执行 this.$nextTick 钩子(执行我们自定义的 nextTick 逻辑)(后面完成的内容都在微任务阶段)
具体描述:Vue异步执行DOM的更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变(将用户同步修改的多个数据缓存起来到watcher队列),如果同一个watcher被多次触发,只会被推入到队列中一次。然后,等同步代码执行完,说明这一次的数据修改结束了,才会去更新对应DOM(在微任务阶段,将之前缓存的watcher队列进行执行,具体的执行还不清楚,这是vue的底层逻辑,但是更新DOM之后就会执行我们自定义的this.nexTick回调)
注意:实际上普通的 html 中对于 DOM 的修改更新是同步的,修改完之后就可以见效了获取最新的 DOM 了!但是 vue 中却是异步的,不能直接获取最新的 DOM!
既然修改 DOM 本来是个同步的过程,那为什么 Vue 却把 DOM 更新变成了异步的(Vue 中异步更新 DOM 的原因)?
因为 Vue 处于性能考虑,一方面可以省去不必要的 DOM 操作,比如同时多次修改一个数据,只需要关心最后一次就好了,另一方面可以将 DOM 操作聚集,提升 render 性能。
为什么 Vue 创造了$nextTick 这么个钩子呢?
基于 Vue 中 DOM 异步更新的特点,导致我们不能实时地,立即地去操作最新的 DOM,为保证我们可以操作最新的 DOM,所以创造了一个 this.$nextTick 函数,用于在 DOM 更新完毕之后进行一些操作!
this.$nextTick 的执行时机
在事件循环的"微任务"阶段执行,也就是在 DOM 更新完成之后、浏览器渲染之后(可以说就是本次 DOM 更新的最后时间节点执行)
this.$nextTick 的应用场景
在需要保证可以操作最新的 dom、需要保证获取最新的 dom 内容状态的时候才去使用 this.$nextTick!
在 methods 方法里面更新或者没有更新 data,但是需要操作 dom 的时候。
在 mounted 或者 updated 里面更新了 data,需要操作 dom 的时候。
Vue 中异步更新的详细底层图解
注意:vue 内部的 nextTick 方法和我们定义的 this.$nextTick 不是一回事,不在一个时间执行!
为什么通过 nextTick 方法能获取最新的 DOM?
当同步任务执行完毕之后,将异步队列中回调函数进行执行(实际上就是主线程执行完毕,把微任务带出来执行了)。在微任务阶段,根据先进先出原则(watcher 队列是先进来的,$nextTick回调后进来的),修改 Data 触发的更新异步队列会先得到执行,执行完成后就生成了新的DOM,接下来执行this.$nextTick
的回调函数时,能获取到更新后的 DOM 元素了。
因为任务队列的特点,保证了this.$nextTick
的执行时机,进而保证了我们可以获取最新的 DOM。
updated()和$nextTick()的区别?各自的应用场景?
$nextTick()
的下一次更新到底是啥意思?
1.概念的区别
nextTick 和 updated 是 Vue.js 中的两个不同的概念。
nextTick 是 Vue.js 异步 DOM 更新后进行操作的特殊钩子。当我们修改了数据后,Vue.js 会异步地更新 DOM,而 nextTick 就是用来判断 DOM 更新是否已经完成的方法。我们可以在 nextTick 的回调函数中获取更新后的 DOM 元素,以便做进一步的逻辑处理。
updated 则是 Vue.js 生命周期函数中的一个钩子函数。在组件更新完成后,updated 会被调用。我们可以在 updated 中对组件进行 DOM 操作,或者与服务器进行交互等等。
2.用法的区别
$nextTick 用法:将回调延迟到下次 DOM 更新的最末尾时间节点执行。在修改数据之后立即使用它,然后等待 DOM 更新。
理解:因为它直接写在 js 代码里面,如果立即执行是不能获取最新 DOM 的,所以它相当于一个异步函数,将回调函数的执行时间推迟到下一次 DOM 更新的末尾阶段执行。这个下一次指的是当前处于一个未开始更新状态下的下一次。
注意:$nextTick 可以直接用在 methods 的方法里面,也可以用在 mounted 或者 updated 这种钩子里面
(1)写在了方法里面:如果方法里面修改 data 了,肯定会直接引起 DOM 更新,那就相当于在本次 DOM 更新执行回调(实际上也算是下一次嘛,只不过是直接开始了);如果方法里面没有修改 data,那么就相当于在下一次 DOM 更新执行回调。
(2)写在了 mounted 或者 updated 钩子里面:这时候用$nextTick,会在本次更新周期里面直接触发(不管有没有对于 data 数据进行修改),唯一的作用可能就是确保获取的 DOM 足够准确,因为 mounted 或者 updated 钩子函数内部获取的 DOM 可能还没有完全渲染出来,可能会导致操作不准确,nextTick 的会相对更加准确一些。
测试:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue Example</title>
<!-- 引入 Vue 的 CDN 链接 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
<div id="app">
<button @click="updateData">Update Data</button>
<p>{{ message }}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'Initial Message'
},
methods: {
updateData() {
this.message = 'Updated Message';
this.$nextTick(() => {
console.log('DOM has been updated.'); //顺序2,先进队列的
});
}
},
updated() {
console.log('Component updated.'); //顺序1
this.$nextTick(() => {
console.log('Next tick inside updated.'); //顺序3,后进队列的
});
}
});
</script>
</body>
点击更新按钮之后的输出顺序:可以看到 Next tick inside updated.确实也是在本次直接输出了!

- updated 用法:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。
一句话:this.$nextTick()可以用作局部的数据更新后DOM更新结束后的操作,全局的可以用updated生命周期函数 ——>
this.$nextTick()
分情况调用,updated()
每次必调用
我对于两者的理解:
因为 nextTick 可以写在方法里面,而且方法触发是有条件的(是否被调用了),所以这样 nextTick 的回调并不会在每次更新的时候都触发,而 updated 里面的内容是在每次更新的时候都触发的;
并且 nextTick 定义在 methods 的方法里面确实不会立即触发(除非同时修改了 data 数据,又写了 nextTick),确实是会延迟到下一次 DOM 更新。
3.触发顺序的区别
触发顺序不同(但是都是在微任务阶段触发,并且都是 DOM 更新完成之后、浏览器渲染之前)。响应式数据发生变化时,Vue 会立刻将更新的全部微任务按照顺序(beforeUpdate() update() updated()
)插入到微任务队列中。之后 Vue 对虚拟 DOM 树进行更新。之后虚拟 DOM 树和真实 DOM 树进行对比更新。但此时浏览器不会立即渲染更新后的真实 DOM 树,而是会先清空微任务队列。在顺序执行微任务时,在updated()
执行结束后会立即执行nextTick()
, 即便后面还有别的微任务。因此执行顺序updated()
在前,nextTick()
在后。
注意:虽然都是 DOM 更新完成之后、浏览器渲染之前触发,但是 nextTick 执行晚于 updated,所以大多数情况下 nextTick 获取的 DOM 更加准确!
this.$nextTick 实际上原理中优先是微任务,那么为什么优先使用微任务?
大白话:不想让 nextTick 里面的针对 DOM 操作的回调拖太久执行,保证尽快执行。
因为微任务一定比宏任务优先执行,如果 nextTick 是微任务,它会在当前同步任务执行完立即执行所有的微任务,也就是修改 DOM 的操作也会在当前 tick 内执行,等本轮 tick 任务全部执行完成,才是开始执行 UI rendering。
如果 nextTick 是宏任务,它会被推进宏任务队列,并且在本轮 tick 执行完之后的某一轮执行,注意,它并不一定是下一轮,因为你不确定宏任务队列中它之前还有所少个宏任务在等待着。所以为了能够尽快更新 DOM,Vue 中优先采用的是微任务,并且在 Vue3 中,它没有了兼容判断,直接使用的是promise.then
微任务,不再考虑宏任务了。
2.Vue 中的 DOM 更新是批量的
也就是说如果同时更新了多个数据(或者对一个数据多次更新),DOM 只会更新一次(只会走一次 updated 钩子)
但是如果有三种类型即同步,微任务(promise),宏任务(setTimeout),那么就是同步的一次(2 个同步一起更新),微任务一次,宏任务一次,三者分别更新!
4.$nextTick 的应用场景
$nextTick
方法在以下场景中经常被使用:
- 在组件的
mounted
钩子中,可以使用$nextTick
来确保在组件挂载后对 DOM 进行操作。 - 在计算属性中,当计算属性依赖的响应式数据发生变化时,可以使用
$nextTick
来获取更新后的 DOM 状态。 - 在侦听器中,监听响应式数据的变化,并使用
$nextTick
来执行操作。
注意:不要混淆 data 的更新与 dom 的更新
data 的更新是立即的(所以可以立即获取最新的 data),因为这只是相当于普通的 js 变量进行赋值,而 data 被应用在模版中时会有对应的 watcher,这个 watcher 会进入队列等待统一进行更新(并且一个 data 的多次变化,即同一个 watcher 被多次触发,只会被推入到队列中一次),等可以执行完所有微任务,然后再更新 dom(所以不能立即获取最新的 dom)
所以说,vue 中数据的更新到底在什么时候完成的?
在 Vue.js 中,数据的更新是在"微任务"阶段完成的。当你修改 Vue 实例中的
data
属性时,Vue 会将数据的更改标记为"脏"(所以我们立即获得的 data 并不一定是最终的 data),然后在事件循环的下一个微任务阶段执行实际的数据更新(实际上就是本次事件循环,因为最后做的就是微任务)。微任务(Microtask)是 JavaScript 中的一个异步执行阶段,它在事件循环的宏任务(Macrotask)执行之后,DOM 渲染之前执行。微任务包括
Promise
回调、MutationObserver
回调以及process.nextTick
(在 Node.js 环境中)等。当你修改 Vue 实例中的
data
属性时,Vue 会将这些修改标记为待处理的更改,在当前宏任务执行完成后,在下一个微任务阶段,Vue 会执行实际的数据更新过程。这样做的好处是,在一个宏任务中可能会进行多次数据更新,但 Vue 会将这些更新合并,只在一个微任务阶段执行一次实际的 DOM 更新,从而提高性能和效率。(主要就是为了减少 DOM 更新的次数)
所以说一个 data 的变化只会合并成一个 Watcher 对象进入队列,进而更新 dom,这就是 vue 的异步更新的核心!
5.宏任务与微任务的进一步理解
二者区别:
- 宏任务:当前调用栈中执行的代码成为宏任务。(主代码快,定时器等等)。 ——> 广义上的宏任务,其实就是普通的任务
- 微任务: 当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件。(promise.then,proness.nextTick 等等)。
- 宏任务中的事件放在 callback queue 中(可以理解为狭义的宏任务),由事件触发线程维护;微任务的事件放在微任务队列中,由 js 引擎线程维护。
本质理解:
宏任务便是 JavaScript 与宿主环境产生的回调,需要宿主环境配合处理并且会被放入回调队列的任务都是宏任务。(本质上就是一些回调)
首先要说明宏任务其实本身是任务(task),为什么这么说呢?因为 ES6 新引入了Promise标准,同时浏览器实现上多了一个microtask微任务概念,作为对照才称宏任务。
一个任务就是指计划由标准机制来执行的任何 JavaScript,如程序的初始化、事件触发的回调等。 除了使用事件,你还可以使用 setTimeout() 或者 setInterval() 来添加任务。
理解:一次 js 事件循环,所有 js 代码都是任务(广义上的宏任务),顺序执行,到了异步代码或者事件回调才排队执行,然后排队时区分微宏,微任务排在当前事件循环后执行,如执行中途有微任务则加入直到微任务全执行完;宏任务下一次 js 事件循环执行,如此循环。
MutationObserver ——> 微任务
MutationObserver
是一个 JavaScript 的 API,用于监视 DOM 树的变化。它可以观察 DOM 节点的增加、删除、属性变化以及文本内容的修改等操作。
MutationObserver
提供了一种异步的机制来跟踪 DOM 的变化,而不需要轮询或使用定时器。这样可以避免不必要的性能开销,并且能够更精确地捕捉 DOM 的变化。
使用MutationObserver
通常需要指定一个回调函数,当观察的 DOM 发生变化时,这个回调函数会被触发。回调函数会接收一个MutationRecord
对象的数组,每个MutationRecord
对象表示一次 DOM 的变化,其中包含了变化的类型、目标节点等信息。
因为同步任务其实本质就是宏任务,只在异步任务的时候才详细区分宏微
所以事件循环可以简化为如下的过程: