深入理解 JavaScript 单线程事件循环机制

作者:交互手记时间:2026-10-22 15:48:15浏览:376

本文解析 JavaScript 的单线程执行模型与事件循环机制。详细区分宏任务(Macro-task)与微任务(Micro-task)的执行优先级,阐述主线程、调用栈与任务队列的交互过程。通过具体代码示例,说明 setTimeout、Promise 及 MutationObserver 在事件循环中的调度规则,并解释点击事件与脚本执行对任务队列的影响。

任务队列与分类

任务队列

主线程负责执行当前正在运行的代码,并维护一个函数调用栈。

  • 宏任务(Macro-task,新版规范称为 task)包含:script(整体代码块)、setTimeout、setInterval、setImmediate、I/O 操作以及 UI 渲染。
  • 微任务(Micro-task,新版规范称为 jobs)包含:process.nextTick、Promise 回调、Object.observe(已废弃)以及 MutationObserver(HTML5 新增特性,注意队列中同一时刻只能存在一个 MutationObserver 任务)。

任务分类

同步任务严格按照代码书写顺序依次执行,只有当前语句执行完毕,才会开始执行后续语句。

异步任务不遵循严格的先后顺序,当代码执行到异步逻辑时,该任务会被放入相应的任务队列中,等待后续时机执行。

单线程事件循环机制

单线程

主线程从执行 script(整体代码)开始第一次事件循环。随后全局上下文进入函数调用栈。当调用栈清空(仅剩全局上下文)时,立即执行所有可执行的 jobs(微任务)。在所有 jobs 执行完毕后,事件循环再次从 task(宏任务)队列中取出一个任务执行,执行完毕后再次清空所有 jobs。此过程不断循环,直至所有任务处理完毕。

执行注意事项

注意事项

  • setTimeout 的最小时间间隔不能低于 4 毫秒,若设置值小于此限制,系统会自动调整为 4 毫秒。
  • DOM 渲染频率通常与显示器刷新率同步,60 Hz 的显示器每 16 毫秒刷新一次,因此 DOM 渲染大约每 16 毫秒执行一次。
  • process.nextTick 任务在 jobs 队列中拥有独立的优先级,会在其他 jobs 任务之前执行。
  • 冒泡事件在子元素事件处理函数执行完成后,会被直接插入到主线程中。如果此时主线程不为空,冒泡事件的处理将优先于 jobs 队列中的任务执行。

经典示例分析

经典示例

示例详解:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

通过鼠标点击


<div class="outer">
 <div class="inner"></div>
</div>
// 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);
// 输出结果
click
mutate
click
mutate
promise
promise
timeout
timeout

进阶--通过js执行


<div class="outer">
 <div class="inner"></div>
</div>
// 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();
// 输出结果
click
click
mutate
promise
promise
timeout
timeout

由于点击事件由 JavaScript 触发,当 inner 的 onClick 函数执行完毕时,inner.click() 语句的作用域尚未退出调用栈,导致主线程调用栈不为空。因此,jobs 队列中的任务(mutate 和 promise)不会立即执行,而是继续执行 outer 的 onClick 函数。待 outer 的 onClick 执行完成后,inner.click() 语句才退出调用栈,随后才开始执行 jobs 队列中的任务。

之所以只有一个 mutate 输出,是因为在 jobs 队列中,同一时刻只能存在一个 MutationObserver 任务。当尝试创建第二个 MutationObserver 时,前一个任务尚未执行,因此不再创建新的实例。

总结

以上所述是小编给大家介绍的JS内部事件机制之单线程原理,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!

作者最新文章