news 2026/9/20 16:51:42

TypeScript Async/Await 深入指南:从生成器原理到 ES5/ES6 编译产物全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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捕获它。

这突然(且神奇地)让异步编程变得像同步编程一样简单。要让这个思想实验成立,需要三样能力:

  1. 暂停函数执行(Ability to pause function execution);
  2. 向函数内部注入一个值(Ability to put a value inside the function);
  3. 在函数内部抛出异常(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 则说明了生成器对象所遵循的迭代器接口(nextreturnthrow)。

三、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);中,awaitPromise<number>转换为number(解包),因此count可以直接以number类型参与后续运算;
  • 运行后每隔 500ms 打印Hello01234,最后打印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,产物中还保留着letconst、箭头函数与原生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 目标多了两个关键变化:

  1. __awaiter保持不变:Promise 调度逻辑与 ES6 目标完全一致(fulfilled/rejected/step);
  2. 新增__generator状态机:ES5 没有原生生成器,编译器把「生成器」手写成一个基于switch+label的有限状态机。dramaticWelcome的函数体被重写为__generator(this, function (_a) { switch (_a.label) { ... } })的形式:
    • case 0:初始化(打印Helloi = 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 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 可用。这通常意味着:

  1. 全局 Promise polyfill:在不原生支持 Promise 的环境中(如老版本 IE / 旧 Node.js),需要先引入 Promise 的 polyfill(例如 es6-promise 这类实现)——async/await 的整个调度都建立在new P(...).then.catch之上,没有全局 Promise 会直接报错;
  2. 告诉 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 版本完全一致

两个版本运行输出一致(Hello01234World!),但 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,可以提炼出四条核心认知:

  1. 本质是生成器await对应yield,Promise 的 settled 结果通过generator.next(value)注入、rejected 结果通过generator.throw(error)抛回,三个能力(暂停、注入值、注入异常)全部由生成器提供;
  2. 版本分水岭:TS 1.7 起支持但仅限target es6;TS 2.1 起可编译到 ES3/ES5,代价是额外的__generator状态机;
  3. 运行前提:全局必须有符合规范的 Promise(必要时 polyfill),同时用lib让 TypeScript 认识 Promise 类型,推荐["dom", "es2015.promise", "es5"]的最小组合;
  4. 可复现验证:仓库 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 🌹

项目地址:https://gitcode.com/gh_mirrors/ty/typescript-book
点击查看免费下载
上一篇:Onlook快速入门指南:5分钟上手可视化开发工具
下一篇:终极免费IDM激活教程:3种简单方法解锁完整下载功能

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 16:48:48

Zbrush高效雕刻必备核心快捷键整理与练习指南

简介&#xff1a;对于ZBrush用户而言&#xff0c;快捷键熟练度直接与建模效率挂钩。这份PDF系统整理了ZBrush常用快捷键&#xff0c;覆盖视图操控、笔刷切换、模型编辑、工具面板调用等高频操作&#xff0c;并附有使用要点。内容按基本操作、编辑、模型处理、其他功能划分&…

作者头像 李华
网站建设 2026/9/20 16:47:51

600美元以内DIY开源四足机器人:树莓派+舵机方案全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 16:40:41

给 Claude Code 配 TaoToken,读透 irqreturn_t 的中断返回

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 16:39:13

UI-TARS Desktop 快速上手指南:5 分钟跑通桌面 GUI 自动化

UI-TARS Desktop 快速上手指南&#xff1a;5 分钟跑通桌面 GUI 自动化 【免费下载链接】UI-TARS-desktop The Open-Source Multimodal AI Agent Stack: Connecting Cutting-Edge AI Models and Agent Infra 项目地址: https://gitcode.com/GitHub_Trending/ui/UI-TARS-deskto…

作者头像 李华