JS的执行原理,一文了解Event Loop事件循环、微任务、宏任务

109 阅读10分钟

image.png

JavaScript作为一门单线程语言,为什么能够高效处理异步任务,并在保持用户体验流畅性的同时,做到响应迅速呢?这背后离不开事件循环(Event Loop)和任务队列(Task Queue)的运行机制。本文将深入剖析这些核心概念,并结合实际开发中的应用场景,全面解读事件循环的工作原理与实践意义。

事件循环

事件循环是JavaScript用来处理异步任务的核心机制,它让单线程的JavaScript既能高效执行任务,又不会阻塞页面的响应。简单来说,事件循环的工作就是:同步任务先执行,异步任务排队等候,等同步任务完成后,再按顺序处理异步任务。

事件循环的流程

  1. 执行同步任务
    主线程会先从上到下执行代码中的同步任务,这些任务会放在一个叫“执行栈”的地方依次处理。
  2. 处理异步任务
    如果遇到异步任务,比如定时器(setTimeout)或网络请求,主线程会把它交给“后台”(如浏览器的 Web API)处理,不会阻塞主线程。
  3. 回调函数入队
    当异步任务完成后,后台会把对应的回调函数放到任务队列中(Task Queue)排队等候。
  4. 任务队列到主线程
    一旦主线程的同步任务执行完,就会检查任务队列中是否有任务。如果有,就按照“先进先出”的规则,把任务一个个拿出来执行。
  5. 重复上述步骤
    这一整套流程会一直循环运行,所以被称为“事件循环”(Event Loop)。

为什么事件循环很重要?

通过事件循环,JavaScript能够在单线程的情况下高效处理异步操作,既避免页面卡顿,又能保持流畅的用户体验。你可以把它想象成一个餐厅里的服务流程:厨师(主线程)专心做菜(同步任务),如果有食材没到(异步任务),会让服务员(后台)去等,等到食材准备好,再通知厨师继续完成。这样厨房的效率就不会被拖慢。

同步任务

同步任务就是按照代码的书写顺序逐步执行的任务,当前任务不完成,后续代码就无法继续执行。这些任务会被直接放入主线程的执行栈中逐一处理,所以同步任务是阻塞式的。

常见的同步任务

  • 函数调用
  • 变量赋值
  • 算术运算
  • 控制台输出(console.log

示例代码解析

console.log('Step 1'); // 同步任务 1
let result = add(2, 3); // 同步任务 2
console.log(result);    // 同步任务 3
console.log('Step 2');  // 同步任务 4

function add(a, b) {
  return a + b;         // 同步任务 2 的子任务
}

执行过程:

  1. 主线程从上到下依次执行代码。
  2. console.log('Step 1') 打印内容后,继续执行下一行代码。
  3. 调用 add(2, 3) 函数,等待函数返回值(5)后,赋值给变量 result
  4. 接着,console.log(result) 打印结果。
  5. 最后,打印 'Step 2'

异步任务

异步任务指的是不直接在主线程中立即执行的任务,而是通过回调函数或其他机制,将其委托给外部环境(如浏览器的 Web API 或 Node.js 的线程池)处理。当异步任务完成后,其回调会被加入任务队列,等待主线程有空时再执行。这种机制让 JavaScript 能够在不阻塞主线程的情况下处理耗时操作,保持页面流畅和响应性。

常见的异步任务类型

  1. 回调函数(Callback)
  2. Promiseasync/await
  3. Generator
  4. 事件监听(Event Listener)
  5. 发布/订阅模式
  6. 计时器(如 setTimeoutsetInterval
  7. requestAnimationFrame
  8. MutationObserver
  9. process.nextTick(Node.js 专有)
  10. I/O 操作(如网络请求、文件读写等)

示例代码解析

console.log('Start'); // 同步任务

setTimeout(() => {
  console.log('Timeout callback'); // 异步任务
}, 1000);

console.log('End'); // 同步任务

执行过程:

  1. 主线程首先执行同步任务,打印 'Start'
  2. 遇到 setTimeout 时,将其回调函数交给浏览器的计时器去处理,计时器开始倒计时(1秒),但主线程不会等待它完成。
  3. 主线程继续执行后续代码,打印 'End'
  4. 当 1 秒倒计时结束后,计时器将回调函数加入任务队列。
  5. 主线程的同步任务执行完毕后,检查任务队列,将回调函数放入执行栈并执行,最终打印 'Timeout callback'

输出结果:

Start
End
Timeout callback

任务队列详解

任务队列(Task Queue)是存放异步任务回调函数的地方,主线程执行完同步任务后,会按照先进先出的顺序,从任务队列中取出任务并执行。

宏任务(Macro Task)与微任务(Micro Task)

JavaScript 中的异步任务分为两类:

  1. 宏任务(Macro Task)
    setTimeoutsetIntervalI/O 操作 等。
  2. 微任务(Micro Task)
    Promise.thenasync/awaitprocess.nextTick 等。

微任务的优先级高于宏任务。当主线程执行完当前的同步代码后,会先清空微任务队列,再执行宏任务队列中的第一个任务。

任务队列的类型

JavaScript 的任务队列分为 宏任务队列(macrotask queue)微任务队列(microtask queue) 。事件循环机制会优先处理微任务队列中的所有任务,然后再从宏任务队列中取出下一个宏任务执行,重复这一过程,形成事件循环(Event Loop)。

1. 宏任务(Macrotask)

宏任务是较大粒度的任务,通常会触发独立的事件循环阶段。包括以下内容:

  • 所有同步任务
    (虽然同步任务不进入任务队列,但它们也是宏任务的一部分)

  • I/O 操作
    如文件读写、数据库操作等。

  • 计时器

    • setTimeout
    • setInterval
    • setImmediate(仅限 Node.js 环境)
  • 动画帧

    • requestAnimationFrame
  • 事件监听回调

    • clickkeydown 等事件处理函数。

2. 微任务(Microtask)

微任务是较小粒度且优先级更高的任务。每个宏任务完成后,事件循环会优先处理微任务队列中的所有任务,直到清空微任务队列。包括以下内容:

  • Promise 的回调

    • thencatchfinally
  • async/await 中的异步代码

    • await 后的部分。
  • MutationObserver

    • 用于监听 DOM 变化的回调。
  • process.nextTick

    • (仅限 Node.js 环境)

执行优先级

事件循环的顺序:

  1. 执行主线程中的同步代码(这是一个宏任务)。
  2. 清空微任务队列中的所有任务。
  3. 执行下一个宏任务。
  4. 回到第 2 步,循环往复。

示例代码解析

console.log('Script start'); // 同步任务

setTimeout(() => {
  console.log('setTimeout callback'); // 宏任务
}, 0);

Promise.resolve().then(() => {
  console.log('Promise callback'); // 微任务
});

console.log('Script end'); // 同步任务

执行过程:

  1. 执行同步任务,打印 'Script start''Script end'
  2. setTimeout 的回调进入宏任务队列,Promise 的回调进入微任务队列。
  3. 清空微任务队列,执行 Promise callback,打印 'Promise callback'
  4. 执行宏任务队列中的任务,打印 'setTimeout callback'

输出结果:

Script start
Script end
Promise callback
setTimeout callback

任务执行过程

在 JavaScript 中,所有任务都在主线程上执行,任务的执行可以分为 同步任务异步任务 两个阶段。特别是对于 异步任务,其处理过程涉及两个重要的阶段:Event Table(事件表)Event Queue(事件队列)

事件表(Event Table)

事件表保存了宏任务的相关信息,包括事件监听器和它们对应的回调函数。当特定类型的事件发生时,相关的回调函数会被添加到事件队列中,等待执行。例如,你可以使用 addEventListener 将事件监听器注册到事件表中:

document.addEventListener('click', function() {
  console.log('Hello world!');
});

这里,当用户点击页面时,'click' 事件的回调会被加入事件队列。


事件队列(Event Queue)与微任务

事件队列存储的是事件的回调函数,当事件发生时,相应的回调函数会被添加到队列中。微任务则与事件队列密切相关。JavaScript 引擎在执行完同步任务后,会检查事件队列。如果事件队列中有任务,它会依次取出并执行。

任务队列的执行流程

  1. 同步任务 在主线程执行,不会排队,它们会按照代码的顺序依次执行。

  2. 异步任务 分为宏任务和微任务:

    • 宏任务进入宏任务队列,微任务进入微任务队列。
  3. 执行宏任务:主线程首先执行一个宏任务。

  4. 执行完宏任务后,执行当前层的微任务。微任务队列中的任务会优先于下一个宏任务执行。

  5. 继续执行下一个宏任务,重复以上步骤,直到所有任务完成。

这种流程确保了异步任务能够在适当的时机插入执行,并且 JavaScript 能够高效地处理任务,同时保持程序的响应性。


示例代码解析

console.log('Start'); // 同步任务

setTimeout(() => {
  console.log('setTimeout callback'); // 宏任务
}, 0);

Promise.resolve().then(() => {
  console.log('Promise callback'); // 微任务
});

console.log('End'); // 同步任务

执行过程:

  1. 同步任务:首先执行 console.log('Start')console.log('End')
  2. setTimeout 是宏任务,会被添加到宏任务队列中等待执行。
  3. Promisethen 方法是微任务,回调会被加入微任务队列。
  4. 执行顺序:当主线程执行完同步任务后,会先处理微任务队列中的任务(打印 'Promise callback')。
  5. 宏任务执行:最后,宏任务队列中的回调函数会被执行(打印 'setTimeout callback')。

输出结果:

Start
End
Promise callback
setTimeout callback

示例代码解析(很重要!!! 这也是面试常考笔试题)

console.log(1);

setTimeout(() => {
    console.log(2);
}, 0);

console.log(3);

new Promise((resolve) => {
    console.log(4);
    resolve();
    console.log(5);
}).then(() => {
    console.log(6);
});

console.log(7);

执行顺序解析:
1 => 3 => 4 => 5 => 7 => 6 => 2

1. 同步任务执行顺序

  • console.log(1) 打印 1
  • setTimeout 的回调被添加到宏任务队列(它的回调会在所有同步任务执行完后执行,但会先于 Promisethen 回调执行)。
  • console.log(3) 打印 3

2. 创建 Promise 实例

  • 创建 Promise 实例是同步的,因此以下代码也会同步执行:

    • console.log(4) 打印 4
    • resolve() 会调用 then 回调,但 then 本身是异步的,会将回调加入微任务队列。
    • console.log(5) 打印 5

3. 执行微任务队列

  • 当同步任务(包括创建 Promise 的同步部分)执行完毕后,事件循环会首先检查并执行微任务队列。
  • then 方法的回调会被加入微任务队列,因此 console.log(6) 会在所有同步任务执行完后立即打印。

4. 执行宏任务队列

  • setTimeout 的回调放入宏任务队列,它会在微任务队列执行完后,按照先进先出的顺序执行,最终打印 2

最终输出顺序

1
3
4
5
7
6
2

解释:

  1. 同步任务console.log(1)console.log(3)console.log(4)console.log(5)console.log(7) 会依次执行。
  2. 微任务Promise.then 中的回调 (console.log(6)) 会在同步任务执行完毕后立即执行。
  3. 宏任务setTimeout 的回调 (console.log(2)) 会在所有同步任务和微任务完成后执行。

注意:

  • async/await 是基于 Promise 实现的,await 让后续代码变成微任务,所以 async/awaitPromise/then 的行为相同。

总结

希望本文对你有所帮助,如果你有任何疑问、建议或者其他,欢迎在评论区留言。