TypeScript是如何工作的

发表于 3年以前  | 总阅读数:324 次

TypeScript 是一门基于 JavaScript 拓展的语言,它是 JavaScript 的超集,并且给 JavaScript 添加了静态类型检查系统。TypeScript 能让我们在开发时发现程序中类型定义不一致的地方,及时消除隐藏的风险,大大增强了代码的可读性以及可维护性。相信大家对于如何在项目中使用 TypeScript 已经轻车熟路,本文就来探讨简单探讨一下 TypeScript 是如何工作的,以及有哪些工具帮助它实现了这个目标。

一、TypeScript 工作原理

peScript 的大致工作原理如上图所示:

  1. TypeScript 源码经过扫描器扫描之后变成一系列 Token;
  2. 解析器解析 token,得到一棵 AST 语法树;
  3. 绑定器遍历 AST 语法树,生成一系列 Symbol,并将这些 Symbol 连接到对应的节点上;
  4. 检查器再次扫描 AST,检查类型,并将错误收集起来;
  5. 发射器根据 AST 生成 JavaScript 代码。

可见,AST 是整个类型验证的核心。如对于下面的代码

var a = 1;
function func(p: number): number {
    return p * p;
}
a = 's'
export {
    func
}

生成 AST 的结构为 AST 中的节点称为 Node,Node 中记录了这个节点的类型、在源码中的位置等信息。不同类型的 Node 会记录不同的信息。如对于 FunctionDeclaration 类型的 Node,会记录 name(函数名)、parameters(参数)、body(函数体)等信息,而对于 VariableDeclaration 类型的 Node,会记录 name(变量名)、initializer(初始化)等信息。一个源文件也是一个 Node —— SourceFile,它是 AST 的根节点。

关于如何从源码生成 AST,以及从 AST 生成最终代码,相关理论很多,本文也不再赘述。本节主要说明一下绑定器的作用和检查器如何检查类型。

简而言之,绑定器的终极目标是协助检查器进行类型检查,它遍历 AST,给每个 Node 生成一个 Symbol,并将源码中有关联的部分(在 AST 节点的层面)关联起来。这句话可能不是很直观,下面来说明一下。

Symbol 是语义系统的基本构造块,它有两个基本属性:members 和 exports。members 记录了类、接口或字面量实例成员,exports 记录了模块导出的对象。Symbols 是一个对象的标识,或者说是一个对象对外的身份特征。如对于一个类实例对象,我们在使用这个对象时,只关心这个对象提供了哪些变量/方法;对于一个模块,我们在使用这个模块时,只关心这个模块导出了哪些对象。通过读取 Symbol,我们就可以获取这些信息。

然后再看看绑定器如何将源码中有关联的部分(在 AST 节点的层面)关联起来。这需要再了解两个属性:Node 的 locals 属性以及 Symbol 的 declarations 属性。对于容器类型的 Node,会有一个 locals 属性,其中记录了在这个节点中声明的变量/类/类型/函数等。如对于上面代码中的 func 函数,对应 FunctionDeclaration 节点中的 locals 中有一个属性 p。而对于 SourceFile 节点,则含有 a 和 func 两个属性。

Symbol 的 declarations 属性记录了这个 Symbol 对应的变量的声明节点。如对于上文代码中第 1 行和第 7 行中的 a 变量,各自创建了一个 Symbol,但是这两个 Symbol 的 declarations 的内容是一致的,都是第一行代码 var a = 1;所对应的 VariableDeclaration 节点。

Symbol 的 declarations 属性是个数组,一般来说,这个数组中只有一个对象。一个违反了这种情况的例子是 interface 声明,TypeScript 中的 interface 声明可以合并。如对于下面的例子

interface T {
    a: string
}
interface T {
    b: number
}

生成的 AST 树为 包含两个 InterfaceDeclaration 节点,这个是符合预期的。但是对于这两个 InterfaceDeclaration 节点,关联的 Symbol 为 两个声明之中的成员发生了合并,declarations 中也含有两条记录。

理解了绑定器的作用之后,相信检查器如何工作的也非常明了了。Node 和 Symbol 是关联的,Node 上含有这个 Node 相关的类型信息,Symbol 含有这个 Node 对外暴露的变量,以及 Symbol 对应的声明节点。对于赋值操作,检查给这个 Node 赋的值是否匹配这个 Node 的类型。对于导入操作,检查 Symbol 是否导出了这个变量。对于对象调用操作,先从 Symbol 的 members 属性找到调用方法的 Symbol,根据这个 Symbol 找到对应的 declaration 节点,然后循环检查。具体实现这里就不再研究。

检查结果被记录到 SourceFile 节点的 diagnostics 属性中。

二、TypeScript 与 VSCode

当我们在 VSCode 中新建一个 TypeScript 文件并输入 TS 代码时,可以发现 VSCode 自动对代码做了高亮,甚至在类型不一致的地方,VSCode 还会进行标红,提示类型错误。 这是因为 VSCode 内置了对 TypeScript 语言的支持,类型检查主要通过 TypeScript 插件(extension)进行。插件背后就是 Language Service Protocal。

Language Service Protocal

LSP 是由微软提出的的一个协议,目的是为了解决插件在不同的编辑器之间进行复用的问题。LSP 协议在语言插件和编辑器之间做了一层隔离,插件不再直接和编辑器通信,而是通过 LSP 协议进行转发。这样在遵循了 LSP 的编译器中,相同功能的插件,可以一次编写,多处运行。 从图中可以看出,遵循了 LSP 协议的插件存在两个部分

  1. LSP 客户端,它用来和 VSCode 环境交互。通常用 JS/TS 写成,可以获取到 VSCode API,因此可以监听 VSCode 传过来的事件,或者向 VSCode 发送通知。
  2. 语言服务器。它是语言特性的核心实现,用来对文本进行词法分析、语法分析、语义诊断等。它在一个单独的进程中运行。

TypeScript 插件

VSCode 内置了对 TypeScript 的支持,其实就是 VSCode 内置了 TypeScript 插件。 这一点可以从在 Preference 中搜 typescript,能在 Extensions 下面找到 TypeScript 看出。更改这里面的配置,能控制插件的各种行为。

TypeScript 插件也遵循了 LSP 协议。前面提到 LSP 协议是为了让插件一次编写多处运行,这其实更多针对语言服务器部分。这是因为程序分析功能都由语言服务器实现,这一部分的工作量是最大的。本节内容也先从语言服务器说起。

tsserver

TypeScript 插件的语言服务器其实就是一个在独立进程中运行的 tsserver.js 文件。我们可以在 typescript 源码的 src 文件下面找到 tsserver 文件夹,这个文件夹编译之后,就是我们项目中的 node_modules/typescript/lib/tsserver.js 文件。tsserver 接收插件客户端传过来的各种消息,将文件交给 typescript-core 分析处理,处理结果回传给客户端后,再由插件客户端交给 VSCode,进行展示/执行动作等。

由于 TypeScript 插件不需要将 TS 文件编译成 JS 文件,所以 typescript-core 只会运行到检查器这一步。

private semanticCheck(file: NormalizedPath, project: Project) {
    // 简化了
    const diags = project.getLanguageService().getSemanticDiagnostics(file).filter(d => !!d.file);
    this.sendDiagnosticsEvent(file, project, diags, "semanticDiag");
}

基本上看名字就知道这个函数做了什么。

TypeScript 插件创建 tsserver 的语句为

this._factory.fork(version.tsServerPath, args, kind, configuration, this._versionManager)

很明显可以看出是 fork 了一个进程。fork 函数里值得一提的参数是 version.tsServerPath,它是 tsserver.js 文件的路径。当我们将鼠标移到状态栏右下角 TypeScript 的版本上,会提示当前插件使用的 tsserver.js 文件所在路径。 VSCode 内置了最新稳定版本的 typescript,并使用这个版本的 tsserver.js 文件创建语言服务器。对应的是工作区版本——package.json 中依赖的 typescript 的版本。点击状态栏右下角 TypeScript 版本,会弹窗提示切换 tsserver 的版本。如果 tsserver 版本变更,会重新创建语言服务器进程。

LSP 客户端

LSP 客户端的主要作用:

  1. 创建语言服务器;
  2. 作为 VSCode 和语言服务器之间沟通的桥梁。

创建语言服务器主要是 fork 一个进程,与语言服务器沟通通过进程间通信,与 VSCode 沟通通过调用 VSCode 命名空间 api。

像高亮、悬浮弹窗等功能是很多语言都需要的功能,因此 VSCode 预先准备好了 UI 和动作,LSP 客户端只需要提供相应的数据就可以。如对于语法诊断,VSCode 提供了 createDiagnosticCollection 方法,需要语法诊断功能的插件只需要调用这个方法创建一个 DiagnosticCollection 对象,然后将诊断结果按文件添加到这个对象中即可。TypeScript 插件在创建 LSP 客户端时,顺带给这个客户端关联了一个 DiagnosticsManager 对象。

class DiagnosticsManager {

    constructor(owner: string, onCaseInsenitiveFileSystem: boolean) {
        super();
        // 创建了三个对象,_diagnostics和_pendingUpdate主要用作缓存,进行性能优化
        // _currentDiagnostics是诊断结果核心对象,调用了createDiagnosticCollection
        this._diagnostics = new ResourceMap<FileDiagnostics>(undefined, { onCaseInsenitiveFileSystem });
        this._pendingUpdates = new ResourceMap<any>(undefined, { onCaseInsenitiveFileSystem });
        this._currentDiagnostics = this._register(vscode.languages.createDiagnosticCollection(owner));
    }

    public updateDiagnostics(
        file: vscode.Uri,
        language: DiagnosticLanguage,
        kind: DiagnosticKind,
        diagnostics: ReadonlyArray<vscode.Diagnostic>
    ): void {
        // 有简化,给每个文件创建一个fileDiagnostics对象,将诊断结果记录到fileDiagnostics对象中
        // 将file和fileDiagnostics关联到_diagnostics对象中后,触发一个更新事件
        const fileDiagnostics = new FileDiagnostics(file, language);
        fileDiagnostics.updateDiagnostics(language, kind, diagnostics);
        this._diagnostics.set(file, fileDiagnostics);
        this.scheduleDiagnosticsUpdate(file);
    }

    private scheduleDiagnosticsUpdate(file: vscode.Uri) {
        if (!this._pendingUpdates.has(file)) {
            // 延时更新
            this._pendingUpdates.set(file, setTimeout(() => this.updateCurrentDiagnostics(file), this._updateDelay));
        }
    }

    private updateCurrentDiagnostics(file: vscode.Uri): void {
        if (this._pendingUpdates.has(file)) {
            clearTimeout(this._pendingUpdates.get(file));
            this._pendingUpdates.delete(file);
        }
        // 真正触发了更新的代码,从_diagnostics中取出文件关联的诊断结果,并设置到_currentDiagnostics对象中
        // 触发更新
        const fileDiagnostics = this._diagnostics.get(file);
        this._currentDiagnostics.set(file, fileDiagnostics ? fileDiagnostics.getDiagnostics(this._settings) : []);
    }

}

LSP 客户端在收到语言服务器的诊断结果后,调用 DiagnosticsManager 对象的 updateDiagnostics 方法,诊断结果就能在 VSCode 上显示出来了。

三、TypeScript 与 babel

在开发过程中,错误提示功能由 VSCode 提供。但是我们的代码需要经过编译之后才能在浏览器中运行,这个过程中是什么东西处理了 TypeScript 呢?答案是 Babel。Babel 最初是设计用来将 ECMAScript 2015+的代码转换成后向兼容的代码,主要工作就是语法转换和 polyfill。只要 Babel 能识别 TypeScript 语法,就能对 TypeScript 语法进行转换。因此,Babel 和 TypeScript 团队进行了长达一年的合作,推出了@babel/preset-typescript 这个插件。使用这个插件,就能将 TypeScript 转换成JavaScript。

Babel 有两种常见使用场景,一种是直接在 CLI 中调用 babel 命令,另一种是将Babel 和打包工具(如 webpack)结合使用。由于 babel 自身并不具备打包功能,所以直接在命令行中调用 babel 命令的用处不大,本节主要讨论如何在 webpack 中使用 babel 处理 typescript。在 webpack 中使用@babel/preset-typescript 插件非常简单,只需要两步。首先是配置 babel,让它加载@babel/preset-typescript 插件

{
    "presets": ["@babel/preset-typescript"]
}

然后配置 webpack,让 babel 能处理 ts 文件

{
    "rules" [
        {
            "test": /.ts$/,
            "use": "label-loader"
        }
    ]
}

这样的话,webpack 在遇到.ts 文件时,会调用 label-loader 处理这个文件。label-loader 将这个文件转换成标准 JavaScript 文件后,将处理结果交还 webpack,webpack 继续后面的流程。label-loader 是怎么将 TypeScript 文件转换成标准 JavaScript 文件的呢?答案是直接删除掉类型注解。先看一下 babel 的工作流程,babel 主要有三个处理步骤:解析、转换和生成。

  1. 解析:将原代码处理为 AST。对应 babel-parse
  2. 转换:对 AST 进行遍历,在此过程中对节点进行添加、更新、移除等操作。对应 babel-tranverse。
  3. 生成:把转换后的 AST 转换成字符串形式的代码,同时创建源码映射。对应 babel-generator。

在加入@babel/preset-typescript 之后,babel 这三个步骤是如何运行呢

  1. 解析:调用 babel-parser 的 typescript 插件,将源代码处理成 AST。
  2. 转换:babel-tranverse 的过程中会调用 babel-plugin-transform-typescript 插件,遇到类型注解节点,直接移除。
  3. 生成:遇到类型注解类型节点,调用对应输出方法。其它如常。

使用 babel,不仅能处理 typescript,之前 babel 就已经存在的 polyfill 功能也能一并享受。并且由于 babel 只是移除类型注解节点,所以速度相当快。那么问题来了,既然 babel 把类型注解移除了,我们写 TypeScript 还有什么意义呢?我认为主要有以下几点考虑:

  1. 性能方面,移除类型注解速度最快。收集类型并且验证类型是否正确,是一个相当耗时的操作。
  2. babel 本身的限制。本文第一节分析过,进行类型验证之前,需要解析项目中所有文件,收集类型信息。而 babel 只是一个单文件处理工具。Webpack 在调用 loader 处理文件时,也是一个文件一个文件调用的。所以 babel 想验证类型也做不到。并且 babel 的三个工作步骤中,并没有输出错误的功能。
  3. 没有必要。类型验证错误提示可以交给编辑器。

当然,由于 babel 的单文件特性,@babel/preset-typescript 对于一些需要收集完整类型系统信息才能正确运行的 TypeScript 语言特性,支持不是很好,如 const enums 等。完整信息可以查看文档[1]。

四、TSC

VSCode 只提示类型错误,babel 完全不校验类型,如果我们想保证提交到代码仓库的代码是类型正确的,应该怎么做呢?这时可以使用 tsc 命令。

tsc --noEmit --skipLibCheck

只需要在项目中运行这个命令,就可以对项目代码进行类型校验。如果再配合 husky,在 gitcommit 之前先执行一下这个命令,检查一下类型。如果类型验证不通过就不执行 git commit,这样整个开发体验就很完美了。

tsc 命令对应的 TypeScript 版本,就是 node_modules 下安装的 TypeScript 的版本,这个版本可能跟 VSCode 的 TypeScript 插件使用的 tsserver 的版本不一致。这在大多数情况下没有问题,VSCode 内置的 TypeScript 版本一般都比项目中依赖的TypeScript 版本高,TypeScript 是后向兼容的。如果遇到 VSCode 类型检查正常,但是 tsc 命令检查出错,或相反的情况,可以从版本方面排查一下。

五、总结

本文探讨了 TypeScript 的工作原理,以及帮助 TypeScript 在项目开发中发挥作用的工具。希望能给大家一些启发。

附录

  • TypeScript AST Viewer[2]。要确保开启了 Option 中的 Binding 选项。

参考资料

[1]文档: https://babeljs.io/docs/en/babel-plugin-transform-typescript#docsNav

[2]TypeScript AST Viewer: https://ts-ast-viewer.com

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/P1w75JreVsJGbusVFt5KaQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237328次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8175次阅读
 目录