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()返回一个新 Promisethrow或返回一个 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 怎么选
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 封装层
- 对并发请求先想清楚:失败要不要影响整体(
allvsallSettled) - 长链路场景要考虑:超时、取消、重试、并发上限
- 给用户反馈:加载中、失败提示、重试按钮(避免静默失败)


