Skip to content

JavaScript 异步从入门到进阶:事件循环、Promise、async/await

异步的核心目标只有一个:把耗时工作交出去(网络/计时器/IO),主线程继续跑。理解异步,最关键不是背 API,而是搞懂:事件循环如何调度任务

先看大图:事件循环在干什么

事件循环:调用栈、宏任务、微任务

一句话规则:

  • 同一时刻只跑一段 JS(在调用栈里执行)
  • 本轮“宏任务”结束后,会 先清空微任务队列,再进入下一轮宏任务

一个最经典的输出顺序题(理解微任务)

js
console.log('1')

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

Promise.resolve().then(() => {
  console.log('3')
})

console.log('4')

输出顺序是:1 4 3 2

原因:

  • console.log('1')console.log('4') 在当前调用栈直接执行
  • Promise.then微任务,会在本轮结束时先执行
  • setTimeout 回调是 宏任务,要等下一轮事件循环

Promise:不是“语法”,是一种状态机

Promise 代表“未来的结果”,有三个状态:

  • pending:进行中
  • fulfilled:成功(有 value
  • rejected:失败(有 reason

链式调用:then 返回的还是 Promise

js
fetch('/api/user')
  .then((res) => res.json())
  .then((user) => {
    console.log('user', user)
    return user.id
  })
  .then((id) => fetch(`/api/user/${id}/profile`))
  .catch((err) => {
    console.error('request failed', err)
  })

要点:

  • then() 返回一个新 Promise
  • throw 或返回一个 rejected Promise,会进入后续 catch()

async/await:把 Promise 链“写得像同步”

await 只在 async 函数里用。它会暂停当前函数,把后续逻辑放到“微任务”里继续执行。

js
async function loadUser() {
  try {
    const res = await fetch('/api/user')
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    const user = await res.json()
    return user
  } catch (err) {
    console.error(err)
    return null
  }
}

并发:Promise.all / allSettled / race 怎么选

串行 vs 并发

Promise.all:全成功才成功(最快,也最“严格”)

js
const [user, list] = await Promise.all([
  fetch('/api/user').then((r) => r.json()),
  fetch('/api/list').then((r) => r.json()),
])

任意一个失败,整体就会失败(直接 throw)。

Promise.allSettled:要“部分成功”就用它

js
const results = await Promise.allSettled([fetch('/api/a'), fetch('/api/b')])

for (const r of results) {
  if (r.status === 'fulfilled') console.log('ok', r.value)
  else console.warn('fail', r.reason)
}

Promise.race:谁先结束用谁(常用来做超时)

js
function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), ms)),
  ])
}

进阶:控制并发数(别一口气发 200 个请求)

很多场景需要“并发但限流”(例如批量上传/批量查询)。下面是一个够用的并发池:

js
async function asyncPool(limit, tasks) {
  const executing = new Set()
  const results = []

  for (const task of tasks) {
    const p = Promise.resolve().then(task)
    results.push(p)
    executing.add(p)
    p.finally(() => executing.delete(p))

    if (executing.size >= limit) {
      await Promise.race(executing)
    }
  }

  return Promise.all(results)
}

用法(把每个请求包装成函数):

js
const tasks = ids.map((id) => () => fetch(`/api/item/${id}`).then((r) => r.json()))
const data = await asyncPool(5, tasks)

进阶:取消请求(AbortController)

fetch 原生支持取消:

js
const controller = new AbortController()

const p = fetch('/api/user', { signal: controller.signal })
controller.abort() // 触发取消

await p // 会抛出 AbortError

最常见的坑:forEach 里用 await

forEach 不会等待 await,需要顺序执行就用 for...of

js
for (const id of ids) {
  await fetch(`/api/item/${id}`)
}

可以并发就用 Promise.all(注意失败策略):

js
await Promise.all(ids.map((id) => fetch(`/api/item/${id}`)))

实战建议(写得更稳、更可维护)

  • 在“边界层”统一兜底:组件加载函数 / 路由 loader / API 封装层
  • 对并发请求先想清楚:失败要不要影响整体(all vs allSettled
  • 长链路场景要考虑:超时、取消、重试、并发上限
  • 给用户反馈:加载中、失败提示、重试按钮(避免静默失败)

🎉有任何问题,欢迎联系我

WeChat QR Code
WeChat
QQ QR Code
QQ

赣ICP备2023003243号