- 教程
【免费下载链接】typescript-book
:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 🌹
async/await是 TypeScript 中把异步编程「同步化」的核心语法:在await一个 Promise 时挂起函数执行,待 Promise 落定(settled)后再恢复,并自动解包值或抛出异常。本文以 TypeScript Book 仓库中的 docs/async-await.md 为主体,结合 code/async-await 下的完整示例、双目标编译产物与配套 tsconfig,以及仓库中生成器、Promise、编译器发射器等文档,从思想实验讲到生成器本质,再到 ES6/ES5 两种编译产物的逐行剖析,最后给出可复现的运行与配置方案。读完你将彻底理解 async/await 的底层机制,并能自主配置--target es6/--target es5的编译环境。
一、思想实验:让异步代码像同步一样简单
想象一个这样的运行时能力:当我们在 Promise 上使用await关键字时,告诉 JavaScript 运行时暂停当前代码的执行,并且只有当该函数返回的 Promise 落定(settled)时才恢复执行。下面是一段用于思考的实验代码(并非真实语法,仅作思想实验):
// 并非真实代码,只是一个思想实验 async function foo() { try { var val = await getMeAPromise(); console.log(val); } catch(err) { console.log('Error: ', err.message); } }当 Promise 落定时,执行会继续:
- 如果 Promise 是fulfilled(成功),那么
await会返回其值; - 如果 Promise 是rejected(失败),则会在原地同步地抛出一个错误,我们可以用
catch捕获它。
这突然(且神奇地)让异步编程变得像同步编程一样简单。要让这个思想实验成立,需要三样能力:
- 暂停函数执行(Ability to pause function execution);
- 向函数内部注入一个值(Ability to put a value inside the function);
- 在函数内部抛出异常(Ability to throw an exception inside the function)。
而这三样能力,恰恰是生成器(Generator)赋予我们的!
二、真相:async/await 底层就是生成器
这个思想实验其实是真实存在的,TypeScript / JavaScript 中的async/await实现也正是如此——在底层,它使用的就是生成器。
上面那个foo函数可以简单地被包装成下面这样:
const foo = wrapToReturnPromise(function* () { try { var val = yield getMeAPromise(); console.log(val); } catch(err) { console.log('Error: ', err.message); } });其中wrapToReturnPromise做的事情非常简单:执行生成器函数拿到generator对象,然后调用generator.next();如果产出的值是一个promise,就对它调用then+catch,并根据结果调用generator.next(result)或generator.throw(error)。仅此而已!
要理解这段包装代码,需要先掌握仓库中生成器(Generators)一节的三个关键结论:
yield允许生成器函数暂停其执行,并把控制权交给外部系统;- 外部系统可以通过
iterator.next(valueToInject)把值注入生成器函数体内部(此时yield表达式的求值结果就是这个值); - 外部系统可以通过
iterator.throw(error)在yield表达式处抛出异常,函数体内部的try/catch可以捕获它。
对照前面思想实验的三项能力,可以一一对应:yield暂停了执行(暂停函数)、next(value)注入了值(放值进函数)、throw(error)抛出了异常(在函数内抛异常)。正是这三条通信通道,让wrapToReturnPromise这种「生成器 + Promise」的适配器成为可能——async/await不过是把这段样板包装代码交给了编译器自动生成而已。
补充阅读:docs/promise.md 讲解了 Promise 的创建、
.then/.catch订阅、链式调用与Promise.all/Promise.race并行控制流,是理解await解包行为的前置知识;docs/iterators.md 则说明了生成器对象所遵循的迭代器接口(next、return、throw)。
三、TypeScript 对 async/await 的支持历程
- TypeScript 1.7:开始支持
async/await。异步函数以async关键字作为前缀;await会挂起执行,直到异步函数返回的 Promise 被 fulfill 并解包(unwraps)该 Promise 中的值。但当时只支持target es6,即直接转译成ES6 生成器。 - TypeScript 2.1:新增了对ES3 与 ES5 运行时的支持,意味着无论你在什么环境,都可以放心使用 async/await。当然,前提是在全局环境中添加了Promise 的 polyfill。
也就是说:从 TS 2.1 起,async/await 不再要求运行时原生支持生成器——编译器会为旧目标生成一套手写的生成器状态机(即后文的__generator)。
四、实战示例:dramaticWelcome 完整代码
仓库 code/async-await/es6/asyncAwaitES6.ts 与 code/async-await/es5/asyncAwaitES5.ts 提供了同一个示例的 TypeScript 源码(两个文件内容一致,分别用于验证不同target的编译输出)。下面我们来看这段代码,并研究 TypeScript async/await 的写法是如何工作的:
function delay(milliseconds: number, count: number): Promise<number> { return new Promise<number>(resolve => { setTimeout(() => { resolve(count); }, milliseconds); }); } // async function always returns a Promise async function dramaticWelcome(): Promise<void> { console.log("Hello"); for (let i = 0; i < 5; i++) { // await is converting Promise<number> into number const count: number = await delay(500, i); console.log(count); } console.log("World!"); } dramaticWelcome();代码要点:
delay是一个返回Promise<number>的普通函数,内部用setTimeout在指定毫秒后resolve(count);dramaticWelcome被标记为async,其返回类型被推断/声明为Promise<void>——async 函数总是返回一个 Promise;- 循环内
const count: number = await delay(500, i);中,await把Promise<number>转换为number(解包),因此count可以直接以number类型参与后续运算; - 运行后每隔 500ms 打印
Hello、0、1、2、3、4,最后打印World!——整个流程用同步代码的书写方式表达,却拥有异步的时序行为。
注意:以上 TypeScript 源码在 TS 1.7 / TS 2.1 时代的编译行为与当时文档一致;现代版本(TS 2.1+)行为相同,只是 ES5 产物的帮助函数实现细节可能随版本微调,核心机制不变。
五、编译到 ES6(--target es6):__awaiter 辅助函数剖析
使用 code/async-await/es6/tsconfig.json("target": "es6","module": "commonjs",仅编译./asyncAwaitES6.ts)编译后,得到的 code/async-await/es6/asyncAwaitES6.js 完整产物如下:
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator"throw"); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; function delay(milliseconds, count) { return new Promise(resolve => { setTimeout(() => { resolve(count); }, milliseconds); }); } // async function always returns a Promise function dramaticWelcome() { return __awaiter(this, void 0, void 0, function* () { console.log("Hello"); for (let i = 0; i < 5; i++) { // await is converting Promise<number> into number const count = yield delay(500, i); console.log(count); } console.log("World!"); }); } dramaticWelcome();可以看到,--target es6的产物非常接近上一节的思想实验:dramaticWelcome被重写为返回__awaiter(...)的结果,函数体被改写为生成器函数(function* () { ... }),原来的await delay(500, i)直接变成yield delay(500, i)。而__awaiter正是编译器替我们生成的wrapToReturnPromise:
step(generator.next(value)):若生成器产出的是未完成的{ done: false }结果,则把result.value(即 Promise)包装后调用.then(fulfilled, rejected);fulfilled/rejected:分别对应 Promise 成功/失败,成功则generator.next(value)把值注入生成器(yield表达式的值),失败则generator"throw"把异常抛回生成器内部(从而被try/catch捕获);result.done ? resolve(result.value):当生成器走到尽头(done: true),用最终返回值resolve整个 Promise。
由于目标是 ES6,产物中还保留着let、const、箭头函数与原生yield/生成器语法,依赖运行时的原生 ES6 生成器支持。
六、编译到 ES5(--target es5):__generator 状态机
使用 code/async-await/es5/tsconfig.json("target": "es5","module": "commonjs","lib": ["dom", "es2015.promise", "es5"],仅编译./asyncAwaitES5.ts)编译后,得到的 code/async-await/es5/asyncAwaitES5.js 完整产物如下:
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator"throw"); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [0, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; function delay(milliseconds, count) { return new Promise(function (resolve) { setTimeout(function () { resolve(count); }, milliseconds); }); } // async function always returns a Promise function dramaticWelcome() { return __awaiter(this, void 0, void 0, function () { var i, count; return __generator(this, function (_a) { switch (_a.label) { case 0: console.log("Hello"); i = 0; _a.label = 1; case 1: if (!(i < 5)) return [3 /*break*/, 4]; return [4 /*yield*/, delay(500, i)]; case 2: count = _a.sent(); console.log(count); _a.label = 3; case 3: i++; return [3 /*break*/, 1]; case 4: console.log("World!"); return [2 /*return*/]; } }); }); } dramaticWelcome();与 ES6 产物相比,ES5 目标多了两个关键变化:
__awaiter保持不变:Promise 调度逻辑与 ES6 目标完全一致(fulfilled/rejected/step);- 新增
__generator状态机:ES5 没有原生生成器,编译器把「生成器」手写成一个基于switch+label的有限状态机。dramaticWelcome的函数体被重写为__generator(this, function (_a) { switch (_a.label) { ... } })的形式:case 0:初始化(打印Hello,i = 0,跳到label 1);case 1:判断i < 5,不满足则return [3 /*break*/, 4]跳到case 4;满足则return [4 /*yield*/, delay(500, i)]——[4, value]表示「产出值并挂起」,返回{ value, done: false }给外层;case 2:恢复后通过_a.sent()取得注入的值(即delay的 resolve 结果)赋给count,打印后进入case 3;case 3:i++后跳回case 1,形成循环;case 4:打印World!,return [2 /*return*/]表示结束生成器。- 中间出现的
trys/ops数组与case 5/6/7分支,则是为try/catch/finally等结构化控制流服务的(本示例没有使用,因此状态机相对精简)。
由此可以看到:--target es5的代价是更大的运行时辅助代码(多出约 70 行的__generator),换来的是对 ES5 环境(IE 等老浏览器)的兼容。
七、运行前提:Promise polyfill 与 lib 配置
原文特别强调:无论哪种 target 场景,都必须保证运行时在全局环境中有一个符合 ECMAScript 规范的 Promise 可用。这通常意味着:
- 全局 Promise polyfill:在不原生支持 Promise 的环境中(如老版本 IE / 旧 Node.js),需要先引入 Promise 的 polyfill(例如 es6-promise 这类实现)——async/await 的整个调度都建立在
new P(...)、.then、.catch之上,没有全局 Promise 会直接报错; - 告诉 TypeScript Promise 类型存在:通过设置
lib标志,让编译器知道Promise的类型定义,例如:"lib": ["dom", "es2015"](DOM 环境 + ES2015 完整库,含 Promise),或"lib": ["dom", "es2015.promise", "es5"](DOM 环境 + 仅 Promise 库 + ES5 基础库,最小化引入)。
仓库中 code/async-await/es5/tsconfig.json 正是采用第二种写法:
{ "compilerOptions": { "target": "es5", "module": "commonjs", "lib": ["dom", "es2015.promise", "es5"] }, "files": ["./asyncAwaitES5.ts"] }而 code/async-await/es6/tsconfig.json 则针对 ES6 目标省略了lib(ES6 target 默认包含 ES2015 库,Promise 类型天然可用):
{ "compilerOptions": { "target": "es6", "module": "commonjs" }, "files": ["./asyncAwaitES6.ts"] }复现实验
在仓库中即可复现整条链路(命令均在对应子目录执行):
# 编译 ES6 目标 cd code/async-await/es6 tsc -p tsconfig.json # 产出 asyncAwaitES6.js node asyncAwaitES6.js # 输出 Hello / 0 / 1 / 2 / 3 / 4 / World! # 编译 ES5 目标 cd code/async-await/es5 tsc -p tsconfig.json # 产出 asyncAwaitES5.js node asyncAwaitES5.js # 输出结果与 ES6 版本完全一致两个版本运行输出一致(Hello、0、1、2、3、4、World!),但 ES5 产物在运行时没有生成器的情况下也能工作,这正是 TS 2.1 带来的能力。
八、源码佐证:编译器如何发射 __awaiter
如果你对「编译器如何生成这些辅助函数」感兴趣,仓库的 docs/compiler/emitter-functions.md 给出了 TypeScript 编译器发射(emit)阶段的入口与内部状态:
- 发射入口调用链为
emitFiles -> emitFile(jsFilePath, targetSourceFile) -> emitJavaScript(jsFilePath, targetSourceFile); - 在
emitJavaScript内部维护了一系列"是否已发射"的标志位,其中就包括:
let extendsEmitted = false; let decorateEmitted = false; let paramEmitted = false; let awaiterEmitted = false; let tempFlags = 0;从源码结构可以推断:awaiterEmitted用于跟踪当前文件是否已经输出过__awaiter辅助函数,从而保证每个文件里该帮助函数只被声明一次((this && this.__awaiter) || function ...这种写法本身也是一种防重复定义的防御:若已存在则直接复用旧实现)。同理,__generator只会在target低于 ES6 时被发射。这正是我们在第六节 ES5 产物中看到__awaiter+__generator两个帮助函数、而 ES6 产物只有__awaiter的原因。
总结
围绕 TypeScript 的async/await,可以提炼出四条核心认知:
- 本质是生成器:
await对应yield,Promise 的 settled 结果通过generator.next(value)注入、rejected 结果通过generator.throw(error)抛回,三个能力(暂停、注入值、注入异常)全部由生成器提供; - 版本分水岭:TS 1.7 起支持但仅限
target es6;TS 2.1 起可编译到 ES3/ES5,代价是额外的__generator状态机; - 运行前提:全局必须有符合规范的 Promise(必要时 polyfill),同时用
lib让 TypeScript 认识 Promise 类型,推荐["dom", "es2015.promise", "es5"]的最小组合; - 可复现验证:仓库 code/async-await 下的
es6/es5两个目录提供了完全相同的 TypeScript 源码、各自的 tsconfig 与已编译的.js产物,可随时对照阅读或运行验证。
后续若想深入,建议继续阅读仓库中的 docs/generators.md(生成器的双向通信细节)、docs/promise.md(Promise 链式与并行控制流)以及 docs/compiler/emitter-functions.md(发射器实现),它们共同构成了理解 async/await 全貌的完整知识链。
- 教程
【免费下载链接】typescript-book
:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 🌹
相关推荐
TypeScript异步编程:Promise、async/await和生成器详解
TypeScript异步编程:Promise、async/await和生成器详解 TypeScript异步编程是现代JavaScript开发的核心技能,掌握Pr
教程TypeScript 深度指南:从基础到编译器原理全面解析
TypeScript 深度指南:从基础到编译器原理全面解析 引言:为什么需要TypeScript深度掌握? 在现代前端开发中,TypeScript已经成为不可或
教程Babel 插件 transform-async-to-generator 完全指南:将 async/await 编译为 ES2015 生成器
Babel 插件 transform async to generator 完全指南:将 async/await 编译为 ES2015 生成器 本篇技术指南围绕
编译器开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考