本文深入解析 JavaScript 事件循环机制,详细阐述微任务(如 Promise、MutationObserver)与宏任务(如 setTimeout、click)的执行顺序。通过对比手动点击与程序触发 click 事件的差异,揭示执行栈状态对任务队列调度及 DOM 更新触发的影响,帮助开发者准确掌握异步代码执行逻辑。
JS 事件环概述
JS 事件环
JavaScript 程序的运行依赖于事件循环机制,该机制确保在特定事件发生时,能够执行预先注册的回调函数。当事件触发时,JavaScript 会将对应的处理函数压入调用栈执行,执行完毕后弹出。然而,关于事件循环的细节往往被忽视。例如,setTimeout 常被称为定时器,但它本质上与其他事件并无区别,区别仅在于普通事件多由用户交互触发,而 setTimeout 由系统按指定时间自动触发。一个常见的疑问是:若将延迟时间设置为 0,回调是否会立即执行?
需要明确的是,setTimeout、DOM 操作以及 HTTP 请求并不属于 V8 引擎的核心部分,而是由浏览器的 Web API 模块处理。JavaScript 作为单线程语言,其核心执行模型始终是一次只处理一件事,这一基本事实从未改变。
执行原理
执行原理
在 JavaScript 中,所有同步方法均被推入调用栈(Call Stack)执行,执行完毕后从栈中弹出。当遇到异步代码(如 setTimeout、MutationObserver、Promise)时,异步部分由 Web API 接管处理。一旦异步操作完成,其回调函数会被放入相应的任务队列中:Promise 和 MutationObserver 的回调进入微任务队列(Microtask Queue),而 setTimeout、setInterval 和 requestAnimationFrame 则进入宏任务队列(Macrotask Queue)。主线程调用栈清空后,引擎会立即执行微任务队列中的所有任务;待微任务队列清空后,才开始执行下一个宏任务。

经典案例与输出分析
一个经典的例子
html
<div class="outer">
<div class="inner"></div>
</div>
js
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Let's listen for attribute changes on the
// outer element
new MutationObserver(function () {
console.log('mutate');
}).observe(outer, {
attributes: true,
});
// Here's a click listener…
function onClick() {
console.log('click');
setTimeout(function () {
console.log('timeout');
}, 0);
Promise.resolve().then(function () {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
当手动点击 inner 元素时,控制台输出顺序如下:
click promise mutate click promise mutate timeout timeout
截至 2020 年 8 月,Chrome、Edge、Opera 和 Firefox 等主流浏览器的输出结果已统一。但在该版本之前的浏览器中,输出顺序可能存在差异。
程序触发点击的特殊现象
一个奇怪的现象
如果不调用手动点击,而是通过代码 inner.click() 触发点击事件,输出结果将发生显著变化。
click click promise mutate promise timeout timeout
造成上述巨大差异的原因在于触发方式的不同。手动点击并非通过函数调用进入执行栈,因此当 inner 的点击回调执行完毕后,主线程调用栈为空,此时可以直接从队列中取出任务执行,随后事件冒泡触发的 outer 回调才被推入栈中运行。而 inner.click() 是通过函数调用方式触发的,inner 的点击回调执行完毕后,click 函数本身并未从栈中弹出,而是直接继续执行冒泡过程中的下一个回调。由于 outer 的回调中包含设置属性的操作,这不会重复触发 MutationObserver(因为属性值可能相同或观察者逻辑已处理),因此 mutate 仅输出一次。待所有冒泡回调执行完毕后,click 函数才会从栈中弹出。
最后需要注意,浏览器引擎会尝试预先执行某些敏感操作以优化性能。
以上就是浅谈javascript事件环微任务和宏任务队列原理的详细内容,更多关于JavaScript 事件环的资料请关注本站其它相关文章!