Skip to content

代码执行顺序

js
async function async1() {
  console.log("a");
  await async2();
  console.log("b");
}
async function async2() {
  console.log("c");
}
console.log("d");
async1();
setTimeout(() => {
  console.log("e");
}, 0);
new Promise((resolve, reject) => {
  console.log("f");
  resolve();
}).then(() => {
  console.log("g");
});

以下是这段代码的输出顺序以及详细的执行过程解释:

输出顺序:

d
a
c
f
b
g
e

详细解析:

1. 同步代码优先执行

  • JavaScript 是单线程的,代码执行会先按照同步任务顺序执行,遇到异步任务时,会将它们放到任务队列(宏任务或微任务)中等待处理。
  • 代码执行从上到下,逐行解释。

2. 具体执行流程

(1) console.log('d');
  • 这是同步代码,直接执行。
  • 输出:d
(2) async1() 调用
  • 进入 async1 函数:
    • console.log('a'); 是同步代码,直接执行。
    • 输出:a
    • 执行 await async2();
      • 调用 async2()
        • console.log('c'); 是同步代码,直接执行。
        • 输出:c
      • async2() 返回的是一个 Promiseasync 函数总是返回一个 Promise),但没有 await.then 的后续逻辑,因此返回的 Promise 会被忽略。
    • await 会暂停 async1 后续代码(即 console.log('b');)的执行,等待 async2() 返回的 Promise 被解决。
    • async1 的剩余代码会被放入 微任务队列
(3) setTimeout(...)
  • 将回调函数(console.log('e');)放入 宏任务队列,等待事件循环调度。
(4) new Promise(...)
  • 立即执行 new Promise 的同步部分:
    • console.log('f'); 是同步代码,直接执行。
    • 输出:f
    • 调用 resolve(),会将 .then() 的回调放入 微任务队列

3. 事件循环处理任务队列

  • 同步代码执行完后,主线程会检查 微任务队列 并执行所有的微任务。
  • 然后再检查 宏任务队列,执行一个宏任务(如 setTimeout)。
(1) 微任务队列:
  • 按照注册顺序执行微任务:
    1. async1 剩余代码(console.log('b');):
      • 输出:b
    2. Promise.then(...) 的回调(console.log('g');):
      • 输出:g
(2) 宏任务队列:
  • 执行 setTimeout 的回调(console.log('e');):
    • 输出:e

总结

最终输出的顺序为:

d
a
c
f
b
g
e

关键点:

  1. 同步任务优先执行
  2. await 会暂停当前 async 函数后续代码,放入微任务队列
  3. 微任务优先于宏任务Promise.thenasync 的后续代码是微任务,而 setTimeout 是宏任务)。

微任务和宏任务简介

JavaScript 的 事件循环机制(Event Loop)将任务分为 微任务宏任务,它们的主要区别在于 调度优先级执行时机


1. 宏任务 (Macro Task)

  • 定义:宏任务是主线程上执行的任务,每次事件循环会从任务队列中取出一个宏任务并执行。
  • 常见的宏任务
    • setTimeoutsetInterval
    • setImmediate(仅 Node.js 环境支持)
    • I/O 操作(例如文件读写、网络请求的回调)
    • UI 渲染任务(浏览器相关)
    • 主代码块(script 代码块本身)

2. 微任务 (Micro Task)

  • 定义:微任务是一个更高优先级的异步任务。当前宏任务执行完成后,会立即执行所有已存在的微任务,然后再进入下一个宏任务。
  • 常见的微任务
    • Promise.then, Promise.catch, Promise.finally
    • MutationObserver(监听 DOM 变化)
    • queueMicrotask(显式创建微任务)

3. 任务调度顺序

  1. 执行主代码块(属于一个宏任务)。
  2. 清空微任务队列(所有的微任务按顺序执行)。
  3. 执行下一个宏任务(如 setTimeout 回调)。
  4. 重复步骤 2 和 3

示例代码分析

javascript
console.log("start");

setTimeout(() => {
  console.log("macro task 1");
}, 0);

Promise.resolve()
  .then(() => {
    console.log("micro task 1");
  })
  .then(() => {
    console.log("micro task 2");
  });

console.log("end");

输出顺序:

start
end
micro task 1
micro task 2
macro task 1

解释:

  1. 同步代码

    • console.log('start'); 输出 start
    • console.log('end'); 输出 end
  2. 异步代码

    • setTimeout 回调(宏任务)被添加到宏任务队列。
    • Promise.resolve().then(微任务)被添加到微任务队列。
  3. 执行微任务

    • 先清空微任务队列:
      • micro task 1 输出。
      • micro task 2 输出。
  4. 执行宏任务

    • macro task 1 输出。

4. 还有其他任务类型吗?

除了微任务和宏任务外,还有一些其他类型的任务或调度机制:

(1) Idle Tasks

  • 定义:空闲任务用于在浏览器空闲时执行一些不重要的工作。
  • 示例requestIdleCallback,它的回调会在浏览器空闲时调用。

(2) 渲染任务

  • 定义:浏览器会在每一帧中执行渲染操作(如更新 DOM、绘制图形)。
  • 说明
    • 渲染任务和 JavaScript 的任务调度是分离的。
    • 一般渲染任务在微任务清空之后进行。

(3) 队列任务的分类总结

  • 宏任务队列:用于调度主任务(主代码块、计时器回调等)。
  • 微任务队列:用于调度高优先级的异步操作(PromiseMutationObserver 等)。
  • 渲染队列:在微任务完成后,浏览器检查是否需要重新渲染页面。

5. 关系图解

plaintext
┌──────────────────────────────┐
│   宏任务队列                 │
│   [setTimeout, I/O, script]  │
└───────────▲──────────────────┘

┌───────────┴──────────┐
│ 微任务队列           │
│ [Promise, MutationObserver] │
└──────────────────────┘

    渲染任务(如果需要)

   进入下一次事件循环

6. 小结

特性宏任务微任务
执行时机下一轮事件循环当前宏任务结束后立即
优先级较低较高
常见任务来源setTimeout, setInterval, I/OPromise, queueMicrotask
任务队列独立宏任务队列独立微任务队列

了解这些任务类型和调度机制是深入掌握 JavaScript 异步行为的关键!