HelloWorld async/await 教程
async/await 是基于 Promise 的语法糖,能把异步流控制写得像同步。要学会它,先弄清 Promise、回调地狱的痛点、事件循环及微任务顺序;掌握 await 的阻塞感与并发模式(如 Promise.all、race)、错误处理(try/catch 与未捕获拒绝)、取消与超时(AbortController 或自定义 wrapper),并发限制与重试策略则是工程里常见的优化手段。

为什么需要 async/await:从回调到 Promise 到 async/await
先回忆一下 JavaScript 异步历史。最初我们用回调(callback),很快出现“回调地狱”:嵌套层层、异常难捕获、控制流混乱。Promise 出现后,把回调扁平化,链式 then/catch 就能清晰表达顺序,但长链还是不如同步代码直观。async/await 作为语法糖,让异步代码像同步那样书写,同时保留 Promise 的非阻塞特性。
一句话理解
- async function:声明一个函数,返回一个 Promise。
- await expression:暂停当前 async 函数的执行,直到表达式对应的 Promise 完成(resolve/reject)。
核心概念与事件循环(Event Loop)
理解 async/await 前,要知道事件循环、任务队列与微任务(microtasks)如何工作。简单来说:
- 宏任务(macrotasks):比如 setTimeout、setInterval、I/O 回调(Node)等。
- 微任务(microtasks):Promise.then、async/await 的继续执行会排入微任务队列。
当当前执行栈清空时,会先清空所有微任务再去执行下一个宏任务。这也是为什么 await 后面的代码会在当前任务结束后马上执行,而不会被 setTimeout 插队。
示例:微任务与宏任务的执行顺序
// 输出顺序:start -> end -> promise -> timeout
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
如果把 Promise.then 换成 await,效果类似,await 后的代码会作为微任务执行。
从 Hello World 开始:最简 async/await 用法
下面一步步写一个简单的“HelloWorld”异步示例,用来引入常见用法。
function delay(ms) {
return new Promise(resolve => setTimeout(() => resolve(ms), ms));
}
async function hello() {
console.log('before');
const ms = await delay(500);
console.log('after', ms);
}
hello(); // before -> (0.5s) -> after 500
注意:hello() 返回一个 Promise,如果需要知道它完成,可以用 hello().then(…) 或在另一个 async 函数里 await hello()
错误处理:try/catch 与 Promise.catch
await 会把 Promise 的 reject 转换为异常抛出,因此最常见的模式是用 try/catch 包裹:
async function fetchData() {
try {
const res = await fetch('/api/data');
const json = await res.json();
return json;
} catch (err) {
console.error('请求失败', err);
throw err; // 视场景决定是否继续抛出
}
}
如果在外层没有 catch,未处理的 reject 会成为未捕获拒绝(unhandled rejection),Node 或浏览器可以发出警告甚至终止进程。
并发与顺序:如何选择合适的模式
很多初学者以为 await 会并发执行,其实默认是顺序阻塞:await 会等待左边完成再继续。要并发执行多个独立任务,应在等待前先触发所有 Promise,然后再 await 它们。
错误示例(顺序)
for (const url of urls) {
const r = await fetch(url); // 每个请求等待前一个完成,性能低
console.log(await r.json());
}
推荐并发示例
// 启动所有请求(立即返回 Promise),再等待所有完成
const promises = urls.map(url => fetch(url).then(r => r.json()));
const results = await Promise.all(promises);
Promise.all 在任一 Promise reject 时会立即 reject;需要容忍部分失败时可以用 Promise.allSettled 或对每个 Promise 添加错误处理。
并发大小限制(限制并发数量)
在真实项目中,直接并发成百上千个请求会导致资源耗尽。常用策略:
- 批量:把任务分批(chunk)处理。
- 池(pool):维护固定数量的并发工作者。
// 简易批量示例
async function batchProcess(items, batchSize, worker) {
const result = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize).map(worker);
result.push(...await Promise.all(batch));
}
return result;
}
取消与超时:AbortController 与自定义超时
JavaScript 的 Promise 标准并不包含取消功能,但现代 fetch 支持 AbortController。这是实现超时的优雅方式:
function timeout(ms) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), ms);
return { signal: controller.signal, clear: () => clearTimeout(id) };
}
async function fetchWithTimeout(url, ms = 5000) {
const { signal, clear } = timeout(ms);
try {
const res = await fetch(url, { signal });
return await res.json();
} finally {
clear();
}
}
如果使用不支持 AbortController 的库,需要用包装 Promise 的方式实现超时:用 Promise.race 包裹原 Promise 与一个在超时后 reject 的定时器。
高级模式:竞速、优先级、重试
- Promise.race:用于竞速场景,比如选最快的镜像源。
- 重试(retry):遇到临时失败时常用指数回退策略。
- 优先级队列:复杂场景下按优先级调度请求。
async function retry(fn, attempts = 3, delayMs = 200) {
let err;
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (e) {
err = e;
await new Promise(r => setTimeout(r, delayMs * Math.pow(2, i)));
}
}
throw err;
}
常见陷阱与反模式
- 在 forEach 中使用 async/await:Array.prototype.forEach 无法等待异步函数,应使用 for…of 或 map + Promise.all。
- 忘记返回 Promise:async 函数总是返回 Promise,但如果手动封装 Promise,需要确保正确 resolve/reject。
- 未处理的 Promise 拒绝:会导致不可预期的错误。始终捕获或使用全局处理。
- 滥用 await:对不需要等待的表达式也 await,会降低并发性能。
调试技巧
- 在 async 函数里多用 console.log 打时间戳查看顺序。
- Chrome DevTools 与 Node 的内置调试器能显示异步堆栈(注意有时会被转译工具破坏)。
- 捕获错误后打印 err.stack,定位来源。
与其它技术的整合
async/await 常见于以下场景:
- Fetch/axios:网络请求简化。axios 默认返回 Promise。
- 文件I/O(Node):fs/promises 提供基于 Promise 的 API,适合 await。
- 数据库操作:多数现代 DB 驱动提供 Promise 接口,await 更易读。
代码示例:结合真实场景的模式
下面举一个稍复杂的例子:从多个镜像源并发获取数据,取最快返回,若失败则重试其它源,最终汇总成功结果并限制并发。
async function fetchFromMirrors(mirrors, limit = 5) {
const results = [];
const queue = [...mirrors];
async function worker() {
while (queue.length) {
const url = queue.shift();
try {
// 尝试三次
const data = await retry(() => fetchWithTimeout(url, 3000).then(r => r), 3);
results.push({ url, data });
} catch (e) {
console.warn('镜像失败', url);
}
}
}
// 启动限量 worker
await Promise.all(new Array(Math.min(limit, mirrors.length)).fill(null).map(worker));
return results;
}
性能与内存考虑
一些注意点:
- 大量未完成的 Promise 会占用内存(尤其是保存了闭包的大对象),应限制并发。
- 频繁创建短周期 Promise(微任务)在高频场景下会有开销,需评估。
- 在 Node 中,避免让事件循环长时间被微任务占满,以免影响 I/O 调度。
与生成器(Generators)对比
在 async/await 普及前,生成器 + 协程库(如 co)也能写出“同步风格”的异步代码。async/await 内置在语言层面,更直观,调试更好,性能也更优。生成器仍然有其用途(例如自定义控制流),但大多数场景建议用 async/await。
兼容性与转译(Babel/TypeScript)
现代浏览器与 Node(从较新版本)原生支持 async/await。若需兼容旧环境,可通过 Babel 或 TypeScript 转译,注意转译后的代码可能使用生成器或手写 Promise 垫片,调试堆栈会更复杂。
测试与 Mock
- 对 async 函数写单元测试时,测试框架通常识别返回 Promise 的测试函数(或直接用 async 测试)。
- 用 Mock 响应网络请求、数据库调用,避免在测试中依赖真实 I/O。
表格:异步风格比较
| 风格 | 优点 | 缺点 |
| 回调 | 兼容早期平台、简单场景 | 嵌套难维护,异常处理麻烦 |
| Promise 链 | 扁平化控制流、链式处理 | 长链仍不如同步直观 |
| async/await | 可读性高、错误处理像同步 | 易误用导致并发问题,需要理解事件循环 |
实战小贴士(写给开发过程中的你)
- 写 await 前先问一句:“这个操作必须按序吗?”如果不是,先触发 Promise 再 await。
- 对外部接口的调用总是假设可能失败,做好重试与退路。
- 给异步操作设定合理超时,避免长时间挂起占用资源。
- 在长期运行的服务里注意内存增长,排查未释放的闭包或未完成的 Promise。
进一步阅读(书名、资料)
- “You Don’t Know JS”(Kyle Simpson)系列,第二版关于异步与性能的章节。
- MDN 文档中关于 Promise、async function、AbortController 的条目。
写到这儿,可能会有点信息量——但实战里你会发现,async/await 最有意思的地方不是语法本身,而是它把思路变清楚了:把异步拆成多个小步骤、明确每一步的失败策略和并发边界。接下来你只要多写几次模式化代码:超时包装、并发限制、重试策略、错误上报——这些组合起来就是工业级的可靠异步逻辑。慢慢你会习惯在设计时把异步当成一等公民来思考,然后就更容易在复杂系统里找到平衡,偶尔也会遇到一下奇怪的 race condition,需要再回头看事件循环和微任务队列才恍然大悟——像学任何东西一样,经验会帮你把书面知识变成直觉。