Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【TypeScript 演化史 -- 5】将 async/await 编译到 ES3/ES5 (外部帮助库) #152

Open
husky-dot opened this issue Nov 25, 2019 · 0 comments

Comments

@husky-dot
Copy link
Owner

作者:Marius Schulz

译者:前端小智

来源:Marius Schulz


阿里云最近在做活动,低至2折,真心觉得很划算了,可以点击本条内容或者链接进行参与
https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=pxuujn3r

腾讯云最近在做活动,百款云产品低至 1 折,可以点击本条内容或者链接进行参与


为了保证的可读性,本文采用意译而非直译。

自2015年11 发布1.7版以来,TypeScript 已支持 async/await 关键字。编译器使用 yield 将异步函数转换为生成器函数。这意味着咱们无法针对 ES3 或 ES5,因为生成器仅在 ES6 中引入的。

TypeScript 2.1 现在支持将异步函数编译为 ES3 和 ES5。与生成的其余代码一样,它们在所有 JS 环境中运行。(这甚至包括IE6,当然不建议在去兼容这么古老的浏览器了)

使用异步函数

下面是一个简单的函数,它在给定的毫秒数之后解析一个 Promise 。使用内置的 setTimeout 函数在 ms 毫秒过后调用 resolve 回调:

function delay(ms: number) {
  return new Promise<void>(function(resolve) {
    setTimeout(resolve, ms)
  })
}

delay 函数返回一个 promise,调用时可以使用 await 来等待这个 promise,如下所示:

function delay(ms: number) {
  return new Promise<void>(function(resolve) {
    setTimeout(resolve, ms);
  })
}

async function asyncAwait () {
  console.log('开始执行...');

  await delay(1000);

  console.log('1 秒过后')

  await delay(1000);

  console.log('过 2 秒后执行完成');
}

现在,如果调用 asyncAwait 函数,在控制台会看到三个消息,一个接一个,每个消息之间都有一个暂停。

asyncAwait();
// 开始执行...
// 1 秒过后
// 过 2 秒后执行完成

现在,来看一下针对 ES2017,ES2016/ES2015 和 ES5/ES3 时 TypeScript 生成的 JS 代码。

编译 async/await 到 ES2017

异步函数是一种JavaScript语言功能,在 ES2017 中进行标准化。

因此,在面向 ES2017 时,TypeScript 编译器无需将 async/await 重写为其他某种构造,因为两个异步函数均已被原生支持。生成的 JS 代码与 TypeScript 代码相同,除了已除去所有类型注释和空白行:

function delay(ms) {
  return new Promise(function(resolve) {
    setTimeout(resolve, ms);
  })
}

async function asyncAwait () {
  console.log('开始执行...');

  await delay(1000);

  console.log('1 秒过后')

  await delay(1000);

  console.log('过 2 秒后执行完成');
}

这里没什么可说的,这是咱们自己编写的代码,只是没有类型注释。

编译 async/await 到 ES2015/ES2016

针对 ES2015,TypeScript 编译器使用生成器函数和 yield 关键字重写 async/await。它还会生成__awaiter 帮助方法作为异步函数的运行程序。以上 asyncAwait 函数的结果编译成 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"](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('开始执行...');
        yield delay(1000);
        console.log('1 秒过后');
        yield delay(1000);
        console.log('过 2 秒后执行完成');
    });
}

辅助代码的 generated 可以忽略不计,但是也不错。如果想在 Node 6.x 或 7.x 应用程序中使用 async/await,需要的配置中设置targetES2015ES2016

请注意,ES2016 标准化的惟一特性是求幂运算符和 Array.prototype.includes 方法,这里两个方法都不使用。因此,针对 ES2016 生成的 JS 代码与针对 ES2015 生成的代码相同。

编译 async/await 到 ES3/ES5

有趣的地方是,使用 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, 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 = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, 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('开始执行...');
                    return [4 /*yield*/, delay(1000)];
                case 1:
                    _a.sent();
                    console.log('1 秒过后');
                    return [4 /*yield*/, delay(1000)];
                case 2:
                    _a.sent();
                    console.log('过 2 秒后执行完成');
                    return [2 /*return*/];
            }
        });
    });
}
asyncAwait();

里面有很多帮助代码。除了前面已经看到的 __awaiter 函数之外,编译器还注入了另一个名为generator的帮助函数,它使用一个状态机来模拟一个可以暂停和恢复的生成器函数。

注意,为了让各位的代码在 ES3 或 ES5 环境中成功运行,需要提供Promise polyfill,因为 Promise 只在 ES2015 中引入。另外,你必须让TypeScript知道在运行时,它可以找到 Promise 函数。这在上一章TypeScript 2.0:内置类型声明 有讲过了。

有了它,async/await 在所有 JS 引擎中都可以运行。

接下来,来看看如何避免在编译中的每个 TypeScript 文件一遍又一遍地将这些辅助函数写入。

TypeScript 中的外部帮助库

在某些情况下,TypeScript 编译器会将帮助函数注入到在运行时调用的生成输出代码中。每个这样的帮助函数都模拟编译目标(target) (ES3、ES5、ES2015) 本身不支持的特定语言特性的语义。

目前,TypeScript 中有以下帮助函数

  • __extends 用于继承
  • __assign 用于扩展对象属性
  • _rest 用于表示对象的剩余属性
  • 还有一些装饰器 __decorate, __param, __metadata
  • __awaiter__generator 用于 async/await

带有 extends 的 ES6 类的典型用例是如下所示的 React 组件:

import * as React from "react";

export default class FooComponent extends React.Component<{}, {}> {
  render() {
    return <div>Foo</div>;
  }
}

如果配置的 target 是ES5,那么 TypeScript 编译器将生成(emit )以下 JS 代码,其中既不支持 class ,也不支持 extends

"use strict";
var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var React = require("react");
var FooComponent = (function (_super) {
    __extends(FooComponent, _super);
    function FooComponent() {
        return _super.apply(this, arguments) || this;
    }
    FooComponent.prototype.render = function () {
        return (React.createElement("div", null, "Foo"));
    };
    return FooComponent;
}(React.Component));
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = FooComponent;

虽然这种方法对于这样的简单示例很有效,但是它有一个很大的缺点:将 __extends 帮助函数代码注入到使用带有extends语句的类的每个编译文件中。也就是说,为应用程序中每个基于类的 React 组件触发帮助函数。

对于一个包含数十个或数百个 React 组件的中型应用程序,对于__extends 函数来说是大量重复的代码。大量重复的代码也会增加包大小,所以下载时间也会随之增加。

这个问题只会对于其它的帮助的函数也会存在,如开头讲的如何将 async/await 降级到 ES3/ES5 中的 __awaiter__generator 帮助函数也很大。注意,它们被注入到每个使用 async/await 关键字的文件中:

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 };
    }
};

--noEmitHelpers 标志

在 1.5 版中,TypeScript 增加 --noEmitHelpers 标志。当指定此编译器选项时,TypeScript 不会在编译后生成任何帮助函数。这样,捆绑包的大小会减少很多。

下面是之前的 React 组件,用 --noEmitHelpers 标志编译:

"use strict";
var React = require("react");
var FooComponent = (function (_super) {
    __extends(FooComponent, _super);
    function FooComponent() {
        return _super.apply(this, arguments) || this;
    }
    FooComponent.prototype.render = function () {
        return (React.createElement("div", null, "Foo"));
    };
    return FooComponent;
}(React.Component));
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = FooComponent;

注意,对 __extends 的调用仍然存在。毕竟,使 React 组件工作是必需的。如果咱们使用 --noEmitHelpers 标志,那么咱们就需要提供所需的所帮助函数,因为TypeScript 假设它们在运行时可用。

但是,手动跟踪所有这些帮助函数非常麻烦。咱必须检查应用程序需要哪些包,然后以某种方式使它们在包中可用。一点都不好玩了。还好,TypeScript 团队提出了一个更好的解决方案。

--importHelpers 标志和 tslib

TypeScript 2.1 引入了一个新的 --importHelpers 标志,它使编译器从tslib(一个外部帮助库)导入帮助函数,而不是将它们内联到每个文件中。安装 tslib 方式如下:

npm install tslib --save

再次编译 Reac t组件,这次使用 --importHelpers 标志:

"use strict";
var tslib_1 = require("tslib");
var React = require("react");
var FooComponent = (function (_super) {
    tslib_1.__extends(FooComponent, _super);
    function FooComponent() {
        return _super.apply(this, arguments) || this;
    }
    FooComponent.prototype.render = function () {
        return (React.createElement("div", null, "Foo"));
    };
    return FooComponent;
}(React.Component));
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = FooComponent;

请注意第2行中的 require("tslib") 调用和第5行中的 tslib_1.__extends 调用。此文件中不再内嵌帮助函数,而是从 tslib 模块导入 __extends 函数。这样,每个帮助函数仅在程序中包含一次,完美。


编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:
https://mariusschulz.com/blog/external-helpers-library-in-typescript
https://mariusschulz.com/blog/compiling-async-await-to-es3-es5-in-typescript

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

clipboard.png

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant