前言
笔者最近在学习Vue的时候,突然对我们平常使用的NextTick原理很感兴趣,他是怎么做到基于更新后的 DOM 状态执行一些逻辑(比如测量某个元素的尺寸),nextTick 是一个非常有用的方法,它允许你在 DOM 更新之后执行代码。
要深入理解 nextTick 的原理,就不得不了解 JavaScript 的任务执行机制,特别是同步代码、宏任务、微任务以及页面渲染之间的优先级关系。
一、JavaScript 任务执行机制基础
(一)任务类型及概念
JavaScript 执行环境基于事件循环(Event Loop)机制,任务主要分为同步任务和异步任务,而异步任务又细分为宏任务和微任务。
- 同步代码:按照代码书写顺序在主线程上依次执行的代码。只有当同步代码全部执行完毕,才会开始处理异步任务。例如简单的变量赋值、函数调用等操作都属于同步代码。
- 宏任务:常见的宏任务有
setTimeout、setInterval、setImmediate(主要在 Node.js 环境)、I/O 操作以及 UI 渲染等。宏任务会在新的事件循环周期开始时被处理。 - 微任务:像
Promise.then、MutationObserver、process.nextTick(Node.js 环境)等都属于微任务。微任务会在当前宏任务执行结束后,下一个宏任务开始之前执行。 - 渲染页面:浏览器会在合适的时机对页面进行重排和重绘操作,以此更新页面的视觉表现。不过,并非每次事件循环都会触发页面渲染,浏览器会根据自身的渲染策略来决定。
(二)任务执行优先级顺序
JavaScript 代码执行时,遵循 “同步代码> 微任务 > 渲染页面 > 宏任务” 的优先级顺序:
-
同步代码优先执行:当代码开始运行,主线程会先执行所有的同步代码,在这个过程中不会处理任何异步任务。例如以下代码:
console.log('同步代码开始');
const result = 3 + 5;
console.log('计算结果:', result);
console.log('同步代码结束');
上述代码中的 console.log 和变量赋值操作会依次执行,直到所有同步代码执行完毕。
2. 处理微任务队列:同步代码执行完成后,事件循环会检查微任务队列。如果队列中有任务,会依次执行队列中的所有微任务,直至队列为空。示例如下:
console.log('同步代码开始');
Promise.resolve().then(() => {
console.log('微任务执行');
});
console.log('同步代码结束');
执行时,先输出 “同步代码开始” 和 “同步代码结束”,然后执行微任务,输出 “微任务执行”。
3. 渲染页面:微任务队列清空后,浏览器会判断是否需要进行页面渲染。如果满足渲染条件(如 DOM 发生变化),则会进行重排和重绘操作。
4. 处理宏任务队列:页面渲染完成后,事件循环会从宏任务队列中取出一个宏任务并执行。执行完该宏任务后,再次检查微任务队列,重复上述执行流程。例如:
console.log('同步代码开始');
setTimeout(() => {
console.log('宏任务执行');
}, 0);
Promise.resolve().then(() => {
console.log('微任务执行');
});
console.log('同步代码结束');
执行顺序为:先执行同步代码,输出 “同步代码开始” 和 “同步代码结束”;接着执行微任务,输出 “微任务执行”;之后浏览器可能进行页面渲染;最后执行宏任务,输出 “宏任务执行”。
送给大家一道大厂面试真题
const p = document.createElement("p");
const body = document.querySelector('body')
p.innerHTML = "p";
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
new Promise((resolve) => {
console.log(3);
resolve()
}).then((res) => {
console.log(4);
});
body.appendChild(p);
大家可以先自己分析一下输出结果哦,我们在文章末尾公布答案
二、Vue.js 的 nextTick 原理与任务执行机制的关联
(一)为何需要 nextTick
Vue 采用异步更新策略,当响应式数据发生变化时,并不会立即将更新应用到 DOM 上。而是把 DOM 更新操作放入队列,等下一个更新周期统一处理。这样做的目的是避免频繁的 DOM 操作,提升性能。所以,在修改数据后立即获取更新后的 DOM 状态,往往得到的是更新前的结果。此时,就需要使用 nextTick 方法,确保在 DOM 更新完成后再执行相应操作。
(二)nextTick 原理与任务执行的结合
-
异步更新队列与任务执行:Vue 在更新 DOM 时采用异步执行方式。当响应式数据改变,与之关联的
watcher对象会被添加到队列中,并且会对队列进行去重处理,防止重复更新。等到同一事件循环内所有数据变化完成,在合适的任务执行阶段统一执行队列中的watcher更新操作,从而更新 DOM。 -
执行时机选择与任务类型:
nextTick的核心目标是在 DOM 更新循环结束后执行回调函数。Vue 会根据不同浏览器环境,选择合适的异步方法来执行回调,这些方法与 JavaScript 的任务类型紧密相关:- Promise.then(微任务) :若浏览器支持
Promise,Vue 优先使用Promise.then实现异步回调。由于Promise.then的回调函数会在当前宏任务执行完毕、下一个宏任务开始前的微任务队列中执行,所以可以保证在 DOM 更新(作为宏任务的一部分)完成后尽快执行nextTick的回调。 - MutationObserver(微任务) :当浏览器不支持
Promise时,Vue 会使用MutationObserver。它同样会将回调函数放入微任务队列中执行,确保在 DOM 更新后执行nextTick回调。 - setImmediate(宏任务) :若上述两种方法都不支持,Vue 会尝试使用
setImmediate。这是在 IE 浏览器中支持的异步方法,属于宏任务,会在当前事件循环结束后立即执行回调。 - setTimeout(宏任务) :作为兜底方案,Vue 会使用
setTimeout,将回调函数放入下一个宏任务队列中执行。
- Promise.then(微任务) :若浏览器支持
-
回调函数管理与任务执行流程:
nextTick会把传入的回调函数存放在数组中。当根据所选的异步方法(微任务或宏任务)到达执行时机时,依次执行数组中的所有回调函数。若未传入回调函数,还会返回一个Promise以支持链式调用。这一过程严格遵循 JavaScript 的任务执行优先级顺序,确保在 DOM 更新完成后执行回调。
三、总结
理解 JavaScript 的任务执行机制对于掌握 Vue.js 的 nextTick 原理至关重要。nextTick 正是巧妙地利用了浏览器的异步机制,根据不同情况选择合适的任务类型(微任务或宏任务),将回调函数放入异步队列,在 DOM 更新循环结束后执行,从而保证开发者能够在 DOM 更新完成后进行相应的操作,避免因异步更新带来的问题,提高代码的正确性和性能。
上文的大厂真题答案为
-
同步代码执行:首先,所有同步代码会依次执行。
- 创建
<p>元素,并设置其 HTML 内容为 "p"。 - 输出
console.log(1),所以你会看到控制台输出1。 - 立即执行
new Promise构造函数内的代码,输出console.log(3),因此接下来你会看到3被打印出来。 - 由于在
Promise构造函数内部调用了resolve(),这将触发.then()方法中的回调函数,但是这个回调会被放入微任务队列中,不会立即执行。 - 最后,将
<p>元素追加到body中。
- 创建
-
微任务执行:一旦当前的同步代码执行完毕,JavaScript 引擎会检查并执行所有已排队的微任务。
- 因此,在此阶段,
.then()方法中的回调将会被执行,输出console.log(4)。此时你将在控制台上看到4。
- 因此,在此阶段,
-
宏任务执行:当所有的微任务都处理完之后,JavaScript 引擎开始处理宏任务队列中的任务。
- 在你的例子中,宏任务就是由
setTimeout设置的任务。它的回调函数被安排在事件循环的下一个周期执行,因此它会在所有微任务完成之后才被执行,输出console.log(2)。这意味着最后你会看到2出现在控制台上。
- 在你的例子中,宏任务就是由
综上所述,最终的控制台输出顺序将是:
1
3
4
2