前端 JavaScript 事件循环 Event Loop 机制详解

作者:小括号

时间:2026-10-23

浏览:586

本文详细解析 JavaScript 单线程模型下的事件循环机制。通过对比宏任务与微任务的执行队列,结合 Promise、setTimeout 及 async/await 示例代码,阐明代码输出的具体顺序。同时探讨 requestAnimationFrame 的执行时机及定时器延迟的不确定性,帮助开发者掌握异步编程的核心逻辑。

文章目录

前言

前言

JavaScript 采用单线程执行模型,那么异步代码是如何被处理的呢?以下面的代码为例,我们来分析其输出结果。


console.log(1);
setTimeout(function() {
 console.log(2);
}, 0);
new Promise(function(resolve) {
 console.log(3);
 resolve(Date.now());
}).then(function() {
 console.log(4);
});
console.log(5);
setTimeout(function() {
 new Promise(function(resolve) {
  console.log(6);
  resolve(Date.now());
 }).then(function() {
  console.log(7);
 });
}, 0);

在运行代码之前,可以先尝试预测最终的输出顺序,随后我们将展开具体的分析过程。

宏任务与微任务

1. 宏任务与微任务

基于编写 Ajax 的经验,JavaScript 通常按语句顺序执行。当遇到异步操作时,会先发起请求并继续执行后续代码,待异步结果返回后再执行回调。但内部是如何管理这些任务的执行顺序呢?

在 JavaScript 中,任务被划分为宏任务(macrotask)和微任务(microtask)。这两类任务分别维护独立的队列,并遵循先进先出(FIFO)的策略执行。同步任务在宏任务队列中执行。

常见的宏任务包括:script(整体代码)、setTimeout、setInterval、I/O 操作、UI 交互事件、postMessage、MessageChannel 以及 Node.js 环境下的 setImmediate。

常见的微任务包括:Promise.then、MutationObserver 以及 Node.js 环境下的 process.nextTick。

事件循环的具体执行步骤如下:

  1. 从宏任务队列头部取出一个任务执行;
  2. 在执行过程中,若遇到微任务,则将其加入微任务队列;
  3. 当前宏任务执行完毕后,检查微任务队列。若存在微任务,则依次执行直到队列为空;
  4. 进行 GUI 渲染;
  5. 返回步骤 1,继续处理下一个宏任务,直到宏任务队列执行完毕;

上述四个步骤构成了事件循环检测机制,即 Event Loop。

代码执行顺序分析

回到前文提到的代码示例:


console.log(1);
setTimeout(function() {
 console.log(2);
}, 0);
new Promise(function(resolve) {
 console.log(3);
 resolve(Date.now());
}).then(function() {
 console.log(4);
});
console.log(5);
setTimeout(function() {
 new Promise(function(resolve) {
  console.log(6);
  resolve(Date.now());
 }).then(function() {
  console.log(7);
 });
}, 0);

其执行步骤如下:

  1. 执行 console.log(1),输出 1;
  2. 遇到 setTimeout,将回调函数 console.log(2) 加入宏任务队列等待执行;
  3. 执行 console.log(3),将 Promise.then 中的 console.log(4) 加入微任务队列;
  4. 执行 console.log(5),输出 5;
  5. 再次遇到 setTimeout,将回调函数中的 console.log(6) 和 console.log(7) 相关逻辑加入宏任务队列;
  6. 当前宏任务执行完毕,检查微任务队列,发现存在一个微任务 console.log(4)(在步骤 3 中添加),执行并输出 4;
  7. 从宏任务队列取出下一个任务 console.log(2) 执行,输出 2;
  8. 当前宏任务执行完毕,检查微任务队列,此时为空;
  9. 取出下一个宏任务执行,执行 console.log(6),并将 Promise.then 中的 console.log(7) 加入微任务队列;
  10. 当前宏任务执行完毕,检查微任务队列,存在一个微任务 console.log(7)(在步骤 9 中添加),执行并输出 7;

因此,最终的输出顺序为:1, 3, 5, 4, 2, 6, 7。

耗时操作对 Event Loop 的影响

我们在 Promise.then 中实现一个耗时操作,使执行过程更加直观:


console.log(1);
var start = Date.now();
setTimeout(function() {
 console.log(2);
}, 0);
setTimeout(function() {
 console.log(4, Date.now() - start);
}, 400);
Promise.resolve().then(function() {
 var sum = function(a, b) {
  return Number(a) + Number(b);
 }
 var res = [];
 for(var i=0; i<5000000; i++) {
  var a = Math.floor(Math.random()*100);
  var b = Math.floor(Math.random()*200);
  res.push(sum(a, b));
 }
 res = res.sort();
 console.log(3);
})

在 Promise.then 中,先生成一个包含 500 万个随机数的数组,然后对该数组进行排序。运行这段代码可以发现:立即输出 1,稍等片刻后输出 3,最后输出 2。无论等待多久,输出 3 和 2 的顺序固定,3 总是在 2 之前。这验证了 Event Loop 的第 3 步:必须等待所有微任务执行完毕后,才开始执行下一个宏任务。

此外,这段代码的输出结果颇具意味:


setTimeout(function() {
 console.log(4, Date.now() - start); // 4, 1380 电脑状态的不同,输出的时间差也不一样
}, 400);

虽然设定了 400ms 后输出,但由于前序任务耗时严重,导致后续任务只能顺延。这也说明 setTimeout 和 setInterval 的延时并不精确,它们仅能将任务安排在大约 400ms 后的宏任务队列中,具体执行时间取决于线程是否空闲。若前一个任务包含耗时操作,或有无限微任务加入,则会阻塞后续任务的执行。

async/await 的执行机制

2. async/await

从上述代码可以看出,Promise.then 中的代码属于微任务。那么 async/await 的代码如何执行?以下面代码为例:


function A() {
  return Promise.resolve(Date.now());
}
async function B() {
  console.log(Math.random());
  let now = await A();
  console.log(now);
}
console.log(1);
B();
console.log(2);

实际上,async/await 只是 Promise 与 Generator 的语法糖。将上述代码改写为如下形式,可以看得更清晰:


function B() {
  console.log(Math.random());
  A().then(function(now) {
    console.log(now);
  })
}
console.log(1);
B();
console.log(2);

这样我们就能明确输出的先后顺序:1, 0.4793526730678652(随机数), 2, 1557830834679(时间戳)。

requestAnimationFrame 的执行时机

3. requestAnimationFrame

requestAnimationFrame 属于异步执行方法,但它既不属于宏任务,也不属于微任务。根据 MDN 的定义:

window.requestAnimationFrame() 告知浏览器希望执行一个动画,并要求浏览器在下次重绘之前调用指定的回调函数来更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

requestAnimationFrame 在 GUI 渲染之前执行,但在微任务之后。不过,它不保证在当前帧必须执行,具体在哪一帧执行由浏览器根据当前策略自行决定。

总结

4. 总结

我们需要记住最重要的两点:JavaScript 是单线程的,且存在 Event Loop 循环机制。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对本站的支持。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章