TypeScript Async/Await 深入指南:从生成器原理到 ES5/ES6 编译产物全解析
教程【免费下载链接】typescript-book:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 项目地址https://gitcode.com/gh_mirrors/ty/typescript-book点击查看免费下载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就对它调用thencatch并根据结果调用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): Promisenumber { return new Promisenumber(resolve { setTimeout(() { resolve(count); }, milliseconds); }); } // async function always returns a Promise async function dramaticWelcome(): Promisevoid { console.log(Hello); for (let i 0; i 5; i) { // await is converting Promisenumber into number const count: number await delay(500, i); console.log(count); } console.log(World!); } dramaticWelcome();代码要点delay是一个返回Promisenumber的普通函数内部用setTimeout在指定毫秒后resolve(count)dramaticWelcome被标记为async其返回类型被推断/声明为Promisevoid——async 函数总是返回一个 Promise循环内const count: number await delay(500, i);中await把Promisenumber转换为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.jsontarget: 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(generatorthrow); } 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 Promisenumber 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正是编译器替我们生成的wrapToReturnPromisestep(generator.next(value))若生成器产出的是未完成的{ done: false }结果则把result.value即 Promise包装后调用.then(fulfilled, rejected)fulfilled/rejected分别对应 Promise 成功/失败成功则generator.next(value)把值注入生成器yield表达式的值失败则generatorthrow把异常抛回生成器内部从而被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.jsontarget: 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(generatorthrow); } 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 没有原生生成器编译器把「生成器」手写成一个基于switchlabel的有限状态机。dramaticWelcome的函数体被重写为__generator(this, function (_a) { switch (_a.label) { ... } })的形式case 0初始化打印Helloi 0跳到label 1case 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 3case 3i后跳回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 目标省略了libES6 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对应yieldPromise 的 settled 结果通过generator.next(value)注入、rejected 结果通过generator.throw(error)抛回三个能力暂停、注入值、注入异常全部由生成器提供版本分水岭TS 1.7 起支持但仅限target es6TS 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.mdPromise 链式与并行控制流以及 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 项目地址https://gitcode.com/gh_mirrors/ty/typescript-book点击查看免费下载相关推荐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 生成器 本篇技术指南围绕编译器开发工具上一篇Onlook快速入门指南5分钟上手可视化开发工具下一篇终极免费IDM激活教程3种简单方法解锁完整下载功能创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考