TypeScript 如何将 async/await 编译成 ES3/ES5
早在 2015 年 11 月的时候,Typescript 在 1.7 的版本就已经支持 async/await
关键字了。编译器会把异步函数转换成 generator 函数。然而,这也意味着编译后的代码不能运行在只支持 ES3 或 ES5 的环境中,因为 generator 是 ES2015 才引入的新特性。
好消息是,Typescript 2.1 现在支持将异步函数编译成 ES3/ES5。就和其他编译生成的代码一样,它们可以在任何 Javascript 环境中运行(甚至包括 IE6,虽然我真的不希望你还要被迫支持这么古老的浏览器)。
使用异步函数
下面是一个简单的函数,在给定的毫秒之后会 resolve 一个 promise 的状态。它使用了内置的 setTimeout
函数在传入的 ms 毫秒后调用 resolve
回调函数。
function delay(ms: number) {
return new Promise<void>(function (resolve) {
setTimeout(resolve, ms)
})
}
delay
函数会返回一个 promise,可以被一个调用者 await:
async function asyncAwait() {
console.log('Knock, knock!')
await delay(1000)
console.log("Who's there?")
await delay(1000)
console.log('async/await!')
}
如果你调用 asyncAwait
函数,你将会在控制台中看到三条消息,每条消息会间隔对应的时间顺序打印:
asyncAwait();
// [After 0s] Knock, knock!
// [After 1s] Who's there?
// [After 2s] async/await!
让我们来看下当构建目标是 ES2017、ES2016/ES2015、以及 ES5/ES3 的时候,编译出的 Javascript 代码分别是什么。
编译 async/await 到 ES2017
异步函数这个 Javascript 语言特性已在 ES2017 中被标准化。当构建目标是 ES2017 的时候,Typescript 编译器没必要将 async/await
改写成其形式,因为异步函数已经在这个语言版本中得到原生支持。下面这个编译后的 Javascript 代码除了删除了所有类型标注以及空行,和 Typescript 代码几乎是一样的:
function delay(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms)
})
}
async function asyncAwait() {
console.log('Knock, knock!')
await delay(1000)
console.log("Who's there?")
await delay(1000)
console.log('async/await!')
}
所以这里没有更多可以说的。这就是我们会写的代码,只不过没有类型标注。
编译 async/await 到 ES2015/ES2016
当构建目标是 ES2015 的时候,Typescript 编译器会把 async/await
重写成使用 yield
关键字的 generator 函数。它同时会生成一个 __awaiter
帮助函数作为异步函数的运行器。上面 asyncAwait
函数编译后的 Javascript 代码如下所示:
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'](value))
} 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(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms)
})
}
function asyncAwait() {
return __awaiter(this, void 0, void 0, function* () {
console.log('Knock, knock!')
yield delay(1000)
console.log("Who's there?")
yield delay(1000)
console.log('async/await!')
})
}
生成的帮助函数的代码数量并不是可以忽略不计的,但还是在可接受范围内。如果你想在 Node 6.x 或者 7.x 的应用中使用 async/await
,ES2015 或者 ES2016 应该是你作为构建目标的语言标准。
值得一提的是,ES2016 中唯二标准化的语言特性是求幂操操作符以及 Array.prototype.includes 方法,但在这里都没有被用到。因此,就这个程序构建目标无论是 ES2016 还是 ES2015,生成的 Javascript 代码都是一样的。
编译 async/await 到 ES3/ES5
到这里事情变得更有趣了。如果你在开发浏览器端的客户端应用,你很可能不能把 ES2015(或者更高的语言版本) 作为构建目标,因为浏览器目前还支持得不够好。
在 Typescript 2.1 中,你可以让编译器将异步代码编译成 ES3/ES5。下面是以上代码被编译后的结果:
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'](value))
} 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
return { next: verb(0), throw: verb(1), return: verb(2) }
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(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms)
})
}
function asyncAwait() {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
console.log('Knock, knock!')
return [4 /*yield*/, delay(1000)]
case 1:
_a.sent()
console.log("Who's there?")
return [4 /*yield*/, delay(1000)]
case 2:
_a.sent()
console.log('async/await!')
return [2 /*return*/]
}
})
})
}
我的天,生成了好多的帮助代码!
除了我们前面提到过的的 __awaiter
函数,编译器还注入了另一个帮助函数 __generator
,它使用了一个状态机来模拟 generator 函数可以暂停和继续的特性。
需要注意的是,为了让你的代码能够在 ES3 或 ES5 的环境中正常运行,你还需要提供 Promise
的 polyfill,因为 Promise 是 ES2015 中引入的语言特性。同时你还得让 Typescript 确知在运行时能够找到 Promise
函数。可以查看这篇文章获得更多的信息。
现在你可以让 async/await
运行在所有的 Javascript 引擎中。请继续关注这个系列后面的文章,我会介绍如何避免在编译阶段每个文件都重复生成这些帮助函数。