Typescript 如何将 async/await 编译成 ES3/ES5

2020-11-02

早在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 引擎中。请继续关注这个系列后面的文章,我会介绍如何避免在编译阶段每个文件都重复生成这些帮助函数。

郑超的独立博客