Async/Await 如何通过同步的方式实现异步
Category(分类): JavaScript Status: 已更新
原文从同步/异步、回调地狱、协程、Iterator、Generator 一路讲到
async/await。本文保留这条学习路径,修复原文代码抓取造成的注释粘连、全角标点、迭代器副作用和错误处理问题,并补充现代 Promise 时序、顶层 await、并发控制和请求取消。
原文最后更新:2020-12-14 历史文章:Async/Await 如何通过同步的方式实现异步
一、同步与异步
在一个 ECMAScript agent 的单个执行上下文中,一段 JavaScript job 会连续执行到完成,当前调用栈上的代码不会被另一个 JavaScript job 抢占。这个模型常被简化成“JavaScript 单线程”,但不能据此说整个浏览器或 Node.js 只有一个线程:
- 浏览器可以使用 Worker、SharedWorker 等独立执行环境;
- Node.js 和浏览器宿主可以在其他线程处理 I/O、编解码或调度;
- 不同 agent 之间不能直接共享普通 JavaScript 对象;
- 异步 API 的完成通知会在之后的任务或 Promise job 中继续执行。
下面的循环会阻塞当前执行环境:
function blockingWork() {
const start = performance.now()
for (let i = 0; i < 1e8; i += 1) {
// 模拟 CPU 密集型工作
}
console.log(`blocked for ${Math.round(performance.now() - start)}ms`)
}
function quickWork() {
console.log('quick work')
}
blockingWork()
quickWork()
网络请求、定时器、文件 I/O 等操作通常由宿主提供异步接口。调用者发起操作后可以继续执行其他 JavaScript,待结果准备好时,宿主再把后续回调安排到相应队列。
二、回调与回调地狱
传统回调风格可能这样发送请求:
requestUser('/api/user', (error, user) => {
if (error) {
handleError(error)
return
}
requestOrders(user.id, (error, orders) => {
if (error) {
handleError(error)
return
}
renderOrders(orders)
})
})
function handleError(error) {
console.error(error)
}
当任务逐层依赖时,回调会形成嵌套,错误处理、取消和资源清理也容易重复,这就是通常所说的“回调地狱”。Promise 可以把成功/失败链条扁平化,async/await 则提供了更接近同步控制流的写法。
Node.js 的回调 API 也遵循类似模式:
import { rename } from 'node:fs'
rename('旧文件.txt', '新文件.txt', error => {
if (error) {
throw error
}
console.log('重命名完成')
})
回调本身不是异步的关键;关键在于宿主何时调用它,以及调用结果如何进入事件循环或 Promise job 队列。
三、协程:一个有用但需要限定的类比
协程通常指可以暂停、恢复,并在多个逻辑任务之间协作切换的控制流结构。它可以运行在同一个线程上,但“协程比线程轻量”是实现层面的常见特征,不是所有语言或运行时的统一保证。
在 JavaScript 教学中,可以把 Generator 看作一种可暂停/恢复的执行机制,把异步操作的完成结果作为恢复时传入的值:
生产者:生产数据 → yield,把控制权交给消费者
消费者:消费数据 → yield,把控制权交给生产者
但需要区分:
- Iterator 是遍历协议,不是协程;
- Generator 同时实现可迭代协议和迭代器协议,并提供暂停/恢复;
async/await的标准语义由 AsyncFunction 和 Await 定义,并不是规范要求编译成 Generator;- “async/await 是 Generator 的语法糖”可以作为历史转译或教学类比,但不能当成完整的实现事实。
四、Iterator 迭代器协议
1. 可迭代对象与迭代器
一个对象要能被 for...of、展开语法等使用,通常需要提供 [Symbol.iterator]() 方法。该方法返回一个迭代器;迭代器需要提供 next(),每次返回一个 IteratorResult 对象:
{
value: any,
done: boolean
}
value 和 done 不都要求显式存在:完成时可以省略 value,done 省略时按假值处理。但为了教学清晰,通常会把它们都写出来。
2. 自定义一个不修改原数组的可迭代对象
function createArrayIterable(items) {
const values = Array.from(items)
return {
[Symbol.iterator]() {
let index = 0
return {
next() {
if (index >= values.length) {
return { value: undefined, done: true }
}
return {
value: values[index++],
done: false
}
}
}
}
}
}
const iterable = createArrayIterable(['人月', '神话'])
const iterator = iterable[Symbol.iterator]()
console.log(iterator.next()) // { value: '人月', done: false }
console.log(iterator.next()) // { value: '神话', done: false }
console.log(iterator.next()) // { value: undefined, done: true }
console.log([...iterable]) // ['人月', '神话']
原文通过 arr.length++ 和 shift() 制造结束条件,会修改输入数组并让实现难以理解;上面的索引版本保持输入不变。
不要为了让普通对象可迭代而修改 Object.prototype[Symbol.iterator]:
// 不要这样做:这会污染所有普通对象的原型
// Object.prototype[Symbol.iterator] = function* () {}
如果确实需要迭代一个对象,只给该对象定义自己的迭代器:
const user = {
name: 'amap',
team: 'sharetrip',
*[Symbol.iterator]() {
yield ['name', this.name]
yield ['team', this.team]
}
}
for (const entry of user) {
console.log(entry)
}
3. 手动消费数组迭代器
function printIterable(iterable) {
const iterator = iterable[Symbol.iterator]()
while (true) {
const result = iterator.next()
if (result.done) {
break
}
console.log(result.value)
}
}
printIterable([0, 1, 2, 3])
五、Generator 生成器
生成器函数使用 function* 声明。调用它不会立即执行函数体,而是返回一个既是迭代器又是可迭代对象的 Generator:
function* numbers() {
yield 1
yield 2
return 3
}
const generator = numbers()
console.log(generator.next()) // { value: 1, done: false }
console.log(generator.next()) // { value: 2, done: false }
console.log(generator.next()) // { value: 3, done: true }
console.log(generator.next()) // { value: undefined, done: true }
yield 可以暂停执行;下一次 next(value) 传入的值,会成为上一个 yield 表达式的结果。yield* 可以把控制委托给另一个可迭代对象,不仅限于另一个 Generator:
function* another() {
yield '人月神话'
}
function* gen() {
yield* another()
const a = yield 'hello'
const b = yield a
yield b
}
const g = gen()
console.log(g.next()) // { value: '人月神话', done: false }
console.log(g.next()) // { value: 'hello', done: false }
console.log(g.next('world')) // { value: 'world', done: false }
console.log(g.next('!')) // { value: '!', done: false }
console.log(g.next()) // { value: undefined, done: true }
六、Generator + Promise 处理异步
Generator 不会自动等待 Promise,需要调用方或执行器负责在 Promise 完成后调用 next():
function* loadData() {
const first = yield Promise.resolve({ a: 1 })
console.log('first:', first)
const second = yield Promise.resolve({ b: 2 })
console.log('second:', second)
}
const generator = loadData()
const firstStep = generator.next()
firstStep.value
.then(first => {
const secondStep = generator.next(first)
return secondStep.value
})
.then(second => {
generator.next(second)
})
手写层层 .then() 比较繁琐,所以可以编写一个自动执行器。可靠的执行器需要处理普通值、Promise 拒绝、Generator 内部异常和最终返回值:
function run(generatorFunction) {
return new Promise((resolve, reject) => {
let iterator
try {
iterator = generatorFunction()
} catch (error) {
reject(error)
return
}
function step(method, value) {
let result
try {
result = iterator[method](value)
} catch (error) {
reject(error)
return
}
if (result.done) {
resolve(result.value)
return
}
Promise.resolve(result.value).then(
nextValue => step('next', nextValue),
error => step('throw', error)
)
}
step('next')
})
}
run(function* () {
const first = yield Promise.resolve({ a: 1 })
const second = yield Promise.resolve({ b: 2 })
return { first, second }
}).then(result => {
console.log(result) // { first: { a: 1 }, second: { b: 2 } }
});
如果 Generator 没有在 try...catch 中捕获被 throw() 传入的异常,执行器会让外层 Promise 拒绝。这个执行器只用于理解历史实现,生产代码应直接使用原生 Promise 和 async/await。
七、Async/Await
1. 基本写法
async function 的调用结果始终是 Promise;正常 return 的值会成为 fulfillment value,函数内未捕获的异常会成为 rejection reason:
async function loadUser() {
const response = await fetch('/api/user')
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
}
loadUser()
.then(user => console.log(user))
.catch(error => console.error(error))
原文用 Generator 执行器与 async/await 对比,这个类比可以帮助理解控制流:
async function loadPair() {
const first = await Promise.resolve({ a: 1 })
console.log(first)
const second = await Promise.resolve({ b: 2 })
console.log(second)
return 'done'
}
loadPair().then(value => {
console.log(value) // done
})
2. await 不只对 Promise 生效
原文说“只有 await 后面是 Promise 才会异步执行,其他值同步执行”,这不准确。await 会对操作数进行 Promise 化,并让当前 async 函数暂停;即使操作数是普通值,后续也会在之后的 Promise job 中恢复:
async function demo() {
console.log('inside: before')
const value = await 1
console.log('inside: after', value)
}
console.log('outside: before')
demo()
console.log('outside: after')
// outside: before
// inside: before
// outside: after
// inside: after 1
await 还会吸收 thenable:
const thenable = {
then(resolve) {
resolve('thenable value')
}
}
async function readThenable() {
return await thenable
}
在普通 classic script 中,await 通常只能位于 async 函数;在支持的 ES Module 中还可以使用顶层 await:
// module.mjs
const config = await fetch('/config.json').then(response => response.json())
export default config
顶层 await 会影响该模块及依赖它的模块的评估顺序,应谨慎用于启动关键路径。
3. 错误处理
最直接的方式是使用 try...catch:
async function loadData() {
try {
const response = await fetch('/api/data')
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return await response.json()
} catch (error) {
console.error('load failed', error)
throw error
}
}
如果一个函数需要把错误和值一起返回,可以使用明确的 helper:
async function to(promise) {
try {
return [null, await promise]
} catch (error) {
return [error, undefined]
}
}
async function asyncTask() {
const [error1, result1] = await to(fetch('/api/one'))
if (error1) {
throw new Error('加载第一个接口失败', { cause: error1 })
}
const [error2, result2] = await to(fetch('/api/two'))
if (error2) {
throw new Error('加载第二个接口失败', { cause: error2 })
}
return { result1, result2 }
}
检查错误对象而不是用 if (!result) 判断结果,否则 0、false 和空字符串等合法值会被误认为失败。若使用 helper,请明确参数是 Promise;如果参数是函数,应先调用它。
八、串行与并发
1. 有依赖关系时串行
第二个请求需要第一个请求的结果时,应按顺序等待:
async function fetchJson(url) {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
}
async function loadSequential() {
const user = await fetchJson('/api/user')
const orders = await fetchJson(`/api/users/${user.id}/orders`)
return { user, orders }
}
2. 无依赖关系时并发
多个操作互不依赖时,先创建全部 Promise,再用 Promise.all() 等待:
async function loadConcurrent(urls) {
const requests = urls.map(url => fetchJson(url))
const results = await Promise.all(requests)
return results
}
map() 在这里的作用是同步创建 Promise,不是让 map 自己等待。Promise.all() 会按输入顺序返回结果;如果一个 Promise 拒绝,整体会尽快拒绝,但不会自动取消其他请求。
3. 循环中的 async 函数
forEach 不会等待 async 回调,也不会把回调的 Promise 返回给调用者:
async function wrong(items) {
items.forEach(async item => {
await save(item)
})
console.log('可能早于保存完成')
}
需要串行时使用 for...of:
async function saveSequential(items) {
for (const item of items) {
await save(item)
}
}
需要并发时使用 Promise.all(items.map(...)):
async function saveConcurrent(items) {
await Promise.all(items.map(item => save(item)))
}
如果操作很多,还要考虑并发上限,使用队列或限制并发的工具,而不是一次性创建无限数量的请求。
异步迭代器可以使用 for await...of:
async function readStream(stream) {
for await (const chunk of stream) {
console.log(chunk)
}
}
4. 一个 URL 数组的完整示例
async function loadAll(urls) {
const responses = await Promise.all(
urls.map(async url => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`${url}: HTTP ${response.status}`)
}
return response.json()
})
)
return responses
}
async function loadOneByOne(urls) {
const results = []
for (const url of urls) {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`${url}: HTTP ${response.status}`)
}
results.push(await response.json())
}
return results
}

九、请求取消与资源清理
Promise 本身没有通用的取消方法。Fetch 可以使用 AbortController:
async function loadWithCancel(url, signal) {
const response = await fetch(url, { signal })
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
return response.json()
}
const controller = new AbortController()
const promise = loadWithCancel('/api/data', controller.signal)
// 需要取消时:controller.abort()
promise.catch(error => {
if (error.name !== 'AbortError') {
console.error(error)
}
})
取消请求、清理事件监听器和释放定时器属于应用层生命周期管理,不能因为使用了 async/await 就自动完成。
十、现代兼容性说明
现代浏览器、Node.js、Deno 和 Bun 对 async/await 的支持已经很广泛。需要兼容旧运行环境时,应区分:
- 语法转译:Babel、TypeScript 等把代码转为目标语法;
- 运行时补充:Promise、迭代器等 API 可能需要 polyfill;
- 宿主 API:
fetch、AbortController、顶层 await 等还要查看目标环境; - 打包策略:不同目标浏览器可以生成不同构建结果。
不能把 es6-shim 当成 async/await 语法的转换器,也不要把 Babel 统称为 polyfill。
总结
- 同步代码会阻塞当前 JavaScript 执行环境,异步 API 通过宿主调度后续任务;
- Iterator 是遍历协议,Generator 是可暂停/恢复的迭代器;
async/await可以用 Generator + Promise 近似解释,但标准语义并不等于 Generator 转换;await会统一处理普通值、Promise 和 thenable,并在之后恢复 async 函数;- 独立任务使用
Promise.all并发,有依赖关系时按顺序等待; forEach不等待 async 回调,map应与Promise.all配合;fetch的 HTTP 404/500 通常仍然兑现,需要检查response.ok;- Promise 不自动取消请求,需要
AbortController或其他业务协议; - 文章中的 Generator 执行器和手写代码适合学习原理,生产代码优先使用原生 API。