JavaScript 异步机制详解:Event Loop、Promise、async/await 完全理解教程

108 阅读5分钟

JavaScript 异步机制详解:Event Loop、Promise、async/await 完全理解教程

作为一名 JavaScript 学习者,在学习异步编程时,我曾经无数次被这样的问题折磨:
“为什么 setTimeout 写在前面,却最后执行?”
“await 到底算不算同步?”
“Promise.then 和 setTimeout 谁先执行?”

当我第一次接触 Event Loop 时,感觉它像一个“只可意会不可言传”的概念。直到我开始自己写代码、一步步推演执行顺序,才发现:
Event Loop 并不神秘,它只是在帮 JS 管理:先做什么、后做什么。

这篇文章是我学习 Event Loop 过程中的一次总结,希望能用尽量通俗的语言 + 可运行的代码,把这个概念讲清楚。

一、为什么 JavaScript 需要 Event Loop?

我们先从一个最基础的问题开始。

JavaScript 是单线程的。

这意味着:同一时间,JS 只能做一件事。

如果你在主线程里写了一个非常耗时的同步任务,比如死循环或复杂计算,整个页面都会“卡住”,用户点什么都没反应。

那问题来了:
JS 是如何在「单线程」的前提下,实现定时器、网络请求、点击事件这些“异步能力”的?

答案就是:
Event Loop(事件循环)

二、一个形象的比喻:餐厅厨房模型

在理解 Event Loop 之前,我先用一个比喻帮自己建立直觉。

  • JS 主线程:一个厨师(只能一次做一道菜)
  • 同步代码:厨师手头正在做的菜
  • 异步任务:顾客点的外卖或预约单
  • 任务队列:挂在墙上的订单单子
  • Event Loop:负责决定“下一道做什么菜”的调度员

核心规则很简单:

厨师永远先把手头这道菜做完,再看看有没有“更着急”的订单,最后才去处理普通订单。

这个“更着急”的订单,就是我们接下来要说的 —— 微任务

三、宏任务与微任务:Event Loop 的核心规则

在 Event Loop 中,任务被分为两大类:

3.1 宏任务

可以理解为“正常排队的订单”:

  • 整个 script(代码整体)
  • setTimeout
  • setInterval
  • I/O
  • UI 渲染
3.2 微任务

可以理解为“插队的紧急订单”:

  • Promise.then / catch / finally
  • async / await(await 后面的代码)
  • queueMicrotask
  • MutationObserver
3.3 核心执行规则(非常重要)

一次 Event Loop 循环中:

  1. 执行一个宏任务
  2. 执行完后,立刻清空所有微任务
  3. 如有需要,进行页面渲染
  4. 进入下一个宏任务

微任务一定比下一个宏任务先执行。

image.png

四、从最简单的例子开始理解

示例:setTimeout 一定是异步的吗?
let a = 1

setTimeout(() => {
  a = 2
}, 1000)

console.log(a)

输出结果:

1

原因分析:

  • console.log(a) 是同步代码,立刻执行
  • setTimeout 的回调会被放入 宏任务队列
  • 当前宏任务(script)还没结束,定时器回调不可能执行

setTimeout 不是“延迟执行”,而是“延后排队”。

五、Promise + setTimeout:经典顺序题拆解

console.log(1)

new Promise((resolve) => {
  console.log(2)
  resolve()
}).then(() => {
  console.log(3)
  setTimeout(() => {
    console.log(4)
  }, 0)
})

setTimeout(() => {
  console.log(5)
  setTimeout(() => {
    console.log(6)
  }, 0)
}, 0)

console.log(7)

最终输出:

1 2 7 3 5 4 6
拆解执行过程
  1. 同步代码执行:1 → 2 → 7

  2. 当前宏任务结束,开始清空微任务:

    • 执行 then → 输出 3
    • 注册一个 setTimeout(4)
  3. 执行宏任务队列中的第一个任务:

    • 输出 5
    • 注册 setTimeout(6)
  4. 下一个宏任务:

    • 输出 4
  5. 再下一个宏任务:

    • 输出 6

记住一句话:Promise.then 永远比 setTimeout 先执行(同一轮循环中)。

六、async / await:它真的“同步”吗?

这是我在学习 Event Loop 时,最容易被表象骗到的一点
因为 async / await 的写法太像同步代码了,很容易让人误以为:

“代码会在 await 这里停住,等结果出来再往下执行。”

但只要把下面这段代码的执行顺序真正跑一遍,就会发现事情并不是这样。

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2 end')
}

async1()

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

new Promise((resolve) => {
  console.log('promise')
  resolve()
}).then(() => {
  console.log('then1')
}).then(() => {
  console.log('then2')
})

console.log('script end')

最终输出:

script start
async2 end
promise
script end
async1 end
then1
then2
setTimeout

一步一步看,这段代码到底是怎么执行的?

① 同步代码先全部执行

  • 输出 script start
  • 调用 async1()

② 执行到 await async2()

  • async2() 立刻执行,输出 async2 end

  • 遇到 await

    • 不会阻塞主线程
    • console.log('async1 end') 不会马上执行
    • 它被暂时“放到一边”,等待后续再执行

③ 继续执行当前脚本中的同步代码

  • 创建 Promise,执行 executor,输出 promise
  • Promise 的 .then 回调被加入 微任务队列
  • 输出 script end

到这里为止,当前这轮同步代码已经执行完了。 接下来,Event Loop 开始处理 “延后的任务”

④ 执行微任务队列(按进入顺序)

  • 执行 await 后续代码 → 输出 async1 end
  • 执行第一个 .then → 输出 then1
  • 执行第二个 .then → 输出 then2

⑤ 执行下一个宏任务

  • 执行 setTimeout 回调 → 输出 setTimeout

从这个例子中,你需要真正记住的几点

  • async2() 是普通函数调用,同步执行
  • await 不会阻塞线程
  • await 后面的代码 不会立刻执行
  • 它会等当前同步代码执行完之后再执行
  • await 后的代码和 Promise.then 一样,执行时机非常靠前

用一句话总结: async / await 看起来像同步代码,但它只是改变了代码“什么时候继续执行”,并没有改变 JavaScript 的执行规则。

七、再看一个 async + 定时器的真实场景

function a() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('a')
      resolve()
    }, 1000)
  })
}

function b() {
  console.log('b')
}

async function foo() {
  setTimeout(() => {
    console.log('c')
  }, 1500)

  await a()
  b()
  console.log('hello')
}

foo()

执行顺序(常见情况):

a
b
hello
c

为什么?

  • await a() 等待 Promise resolve
  • b()hello 属于 await 之后的微任务
  • c 是 1500ms 的宏任务,执行更晚

await 后的代码一定早于后续的宏任务执行。


最后:我学到的不是顺序,而是模型

在真正理解 Event Loop 之前,我刷顺序题全靠“死记硬背”;
理解之后,我只需要问自己三个问题:

  1. 这是同步代码吗?
  2. 它是宏任务还是微任务?
  3. 它是在哪一轮 Event Loop中被执行?

如果你也正在学习 JavaScript,希望这篇文章能帮你少走一点弯路。