<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          TypeScript 是如何工作的?

          共 8926字,需瀏覽 18分鐘

           ·

          2021-10-02 16:11

          TypeScript 是一門基于 JavaScript 拓展的語言,它是 JavaScript 的超集,并且給 JavaScript 添加了靜態(tài)類型檢查系統(tǒng)。TypeScript 能讓我們在開發(fā)時發(fā)現(xiàn)程序中類型定義不一致的地方,及時消除隱藏的風險,大大增強了代碼的可讀性以及可維護性。相信大家對于如何在項目中使用 TypeScript 已經(jīng)輕車熟路,本文就來探討簡單探討一下 TypeScript 是如何工作的,以及有哪些工具幫助它實現(xiàn)了這個目標。

          一、TypeScript 工作原理

          9a1e210c1c2d3f1b286aaaf7cf840af2.webppeScript 的大致工作原理如上圖所示:

          1. TypeScript 源碼經(jīng)過掃描器掃描之后變成一系列 Token;
          2. 解析器解析 token,得到一棵 AST 語法樹;
          3. 綁定器遍歷 AST 語法樹,生成一系列 Symbol,并將這些 Symbol 連接到對應的節(jié)點上;
          4. 檢查器再次掃描 AST,檢查類型,并將錯誤收集起來;
          5. 發(fā)射器根據(jù) AST 生成 JavaScript 代碼。

          可見,AST 是整個類型驗證的核心。如對于下面的代碼

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

          生成 AST 的結(jié)構(gòu)為d247169036f51554c77e496dba035724.webpAST 中的節(jié)點稱為 Node,Node 中記錄了這個節(jié)點的類型、在源碼中的位置等信息。不同類型的 Node 會記錄不同的信息。如對于 FunctionDeclaration 類型的 Node,會記錄 name(函數(shù)名)、parameters(參數(shù))、body(函數(shù)體)等信息,而對于 VariableDeclaration 類型的 Node,會記錄 name(變量名)、initializer(初始化)等信息。一個源文件也是一個 Node —— SourceFile,它是 AST 的根節(jié)點。

          關(guān)于如何從源碼生成 AST,以及從 AST 生成最終代碼,相關(guān)理論很多,本文也不再贅述。本節(jié)主要說明一下綁定器的作用和檢查器如何檢查類型。

          簡而言之,綁定器的終極目標是協(xié)助檢查器進行類型檢查,它遍歷 AST,給每個 Node 生成一個 Symbol,并將源碼中有關(guān)聯(lián)的部分(在 AST 節(jié)點的層面)關(guān)聯(lián)起來。這句話可能不是很直觀,下面來說明一下。

          Symbol 是語義系統(tǒng)的基本構(gòu)造塊,它有兩個基本屬性:members 和 exports。members 記錄了類、接口或字面量實例成員,exports 記錄了模塊導出的對象。Symbols 是一個對象的標識,或者說是一個對象對外的身份特征。如對于一個類實例對象,我們在使用這個對象時,只關(guān)心這個對象提供了哪些變量/方法;對于一個模塊,我們在使用這個模塊時,只關(guān)心這個模塊導出了哪些對象。通過讀取 Symbol,我們就可以獲取這些信息。

          然后再看看綁定器如何將源碼中有關(guān)聯(lián)的部分(在 AST 節(jié)點的層面)關(guān)聯(lián)起來。這需要再了解兩個屬性:Node 的 locals 屬性以及 Symbol 的 declarations 屬性。對于容器類型的 Node,會有一個 locals 屬性,其中記錄了在這個節(jié)點中聲明的變量/類/類型/函數(shù)等。如對于上面代碼中的 func 函數(shù),對應 FunctionDeclaration 節(jié)點中的 locals 中有一個屬性 p。而對于 SourceFile 節(jié)點,則含有 a 和 func 兩個屬性。

          Symbol 的 declarations 屬性記錄了這個 Symbol 對應的變量的聲明節(jié)點。如對于上文代碼中第 1 行和第 7 行中的 a 變量,各自創(chuàng)建了一個 Symbol,但是這兩個 Symbol 的 declarations 的內(nèi)容是一致的,都是第一行代碼 var a = 1;所對應的 VariableDeclaration 節(jié)點。

          Symbol 的 declarations 屬性是個數(shù)組,一般來說,這個數(shù)組中只有一個對象。一個違反了這種情況的例子是 interface 聲明,TypeScript 中的 interface 聲明可以合并。如對于下面的例子

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

          生成的 AST 樹為5b0069e970b2e51d7ebb3b0eb658accd.webp包含兩個 InterfaceDeclaration 節(jié)點,這個是符合預期的。但是對于這兩個 InterfaceDeclaration 節(jié)點,關(guān)聯(lián)的 Symbol 為ca11a128ce8d85a9a8f39dc24f7ddbc3.webp兩個聲明之中的成員發(fā)生了合并,declarations 中也含有兩條記錄。

          理解了綁定器的作用之后,相信檢查器如何工作的也非常明了了。Node 和 Symbol 是關(guān)聯(lián)的,Node 上含有這個 Node 相關(guān)的類型信息,Symbol 含有這個 Node 對外暴露的變量,以及 Symbol 對應的聲明節(jié)點。對于賦值操作,檢查給這個 Node 賦的值是否匹配這個 Node 的類型。對于導入操作,檢查 Symbol 是否導出了這個變量。對于對象調(diào)用操作,先從 Symbol 的 members 屬性找到調(diào)用方法的 Symbol,根據(jù)這個 Symbol 找到對應的 declaration 節(jié)點,然后循環(huán)檢查。具體實現(xiàn)這里就不再研究。

          檢查結(jié)果被記錄到 SourceFile 節(jié)點的 diagnostics 屬性中。

          二、TypeScript 與 VSCode

          當我們在 VSCode 中新建一個 TypeScript 文件并輸入 TS 代碼時,可以發(fā)現(xiàn) VSCode 自動對代碼做了高亮,甚至在類型不一致的地方,VSCode 還會進行標紅,提示類型錯誤。197dd38ac9b5ce2012546fc9a4f9cb61.webp這是因為 VSCode 內(nèi)置了對 TypeScript 語言的支持,類型檢查主要通過 TypeScript 插件(extension)進行。插件背后就是 Language Service Protocal。

          Language Service Protocal

          LSP 是由微軟提出的的一個協(xié)議,目的是為了解決插件在不同的編輯器之間進行復用的問題。LSP 協(xié)議在語言插件和編輯器之間做了一層隔離,插件不再直接和編輯器通信,而是通過 LSP 協(xié)議進行轉(zhuǎn)發(fā)。這樣在遵循了 LSP 的編譯器中,相同功能的插件,可以一次編寫,多處運行。a939c27d504d27de542200d298fe512e.webp從圖中可以看出,遵循了 LSP 協(xié)議的插件存在兩個部分

          1. LSP 客戶端,它用來和 VSCode 環(huán)境交互。通常用 JS/TS 寫成,可以獲取到 VSCode API,因此可以監(jiān)聽 VSCode 傳過來的事件,或者向 VSCode 發(fā)送通知。
          2. 語言服務器。它是語言特性的核心實現(xiàn),用來對文本進行詞法分析、語法分析、語義診斷等。它在一個單獨的進程中運行。

          TypeScript 插件

          VSCode 內(nèi)置了對 TypeScript 的支持,其實就是 VSCode 內(nèi)置了 TypeScript 插件。d5ae5f2db1d74d5a0fde7ba5fd7afef6.webp這一點可以從在 Preference 中搜 typescript,能在 Extensions 下面找到 TypeScript 看出。更改這里面的配置,能控制插件的各種行為。

          TypeScript 插件也遵循了 LSP 協(xié)議。前面提到 LSP 協(xié)議是為了讓插件一次編寫多處運行,這其實更多針對語言服務器部分。這是因為程序分析功能都由語言服務器實現(xiàn),這一部分的工作量是最大的。本節(jié)內(nèi)容也先從語言服務器說起。

          tsserver

          TypeScript 插件的語言服務器其實就是一個在獨立進程中運行的 tsserver.js 文件。我們可以在 typescript 源碼的 src 文件下面找到 tsserver 文件夾,這個文件夾編譯之后,就是我們項目中的 node_modules/typescript/lib/tsserver.js 文件。tsserver 接收插件客戶端傳過來的各種消息,將文件交給 typescript-core 分析處理,處理結(jié)果回傳給客戶端后,再由插件客戶端交給 VSCode,進行展示/執(zhí)行動作等。

          由于 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");
          }

          基本上看名字就知道這個函數(shù)做了什么。

          TypeScript 插件創(chuàng)建 tsserver 的語句為

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

          很明顯可以看出是 fork 了一個進程。fork 函數(shù)里值得一提的參數(shù)是 version.tsServerPath,它是 tsserver.js 文件的路徑。當我們將鼠標移到狀態(tài)欄右下角 TypeScript 的版本上,會提示當前插件使用的 tsserver.js 文件所在路徑。f1afb06acf250f757d415756de0d2f20.webpVSCode 內(nèi)置了最新穩(wěn)定版本的 typescript,并使用這個版本的 tsserver.js 文件創(chuàng)建語言服務器。對應的是工作區(qū)版本——package.json 中依賴的 typescript 的版本。點擊狀態(tài)欄右下角 TypeScript 版本,會彈窗提示切換 tsserver 的版本。如果 tsserver 版本變更,會重新創(chuàng)建語言服務器進程。

          LSP 客戶端

          LSP 客戶端的主要作用:

          1. 創(chuàng)建語言服務器;
          2. 作為 VSCode 和語言服務器之間溝通的橋梁。

          創(chuàng)建語言服務器主要是 fork 一個進程,與語言服務器溝通通過進程間通信,與 VSCode 溝通通過調(diào)用 VSCode 命名空間 api。

          像高亮、懸浮彈窗等功能是很多語言都需要的功能,因此 VSCode 預先準備好了 UI 和動作,LSP 客戶端只需要提供相應的數(shù)據(jù)就可以。如對于語法診斷,VSCode 提供了 createDiagnosticCollection 方法,需要語法診斷功能的插件只需要調(diào)用這個方法創(chuàng)建一個 DiagnosticCollection 對象,然后將診斷結(jié)果按文件添加到這個對象中即可。TypeScript 插件在創(chuàng)建 LSP 客戶端時,順帶給這個客戶端關(guān)聯(lián)了一個 DiagnosticsManager 對象。

          class?DiagnosticsManager?{

          ????constructor(owner:?string,?onCaseInsenitiveFileSystem:?boolean)?{
          ????????super();
          ????????//?創(chuàng)建了三個對象,_diagnostics和_pendingUpdate主要用作緩存,進行性能優(yōu)化
          ????????//?_currentDiagnostics是診斷結(jié)果核心對象,調(diào)用了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?{
          ????????//?有簡化,給每個文件創(chuàng)建一個fileDiagnostics對象,將診斷結(jié)果記錄到fileDiagnostics對象中
          ????????//?將file和fileDiagnostics關(guān)聯(lián)到_diagnostics對象中后,觸發(fā)一個更新事件
          ????????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);
          ????????}
          ????????//?真正觸發(fā)了更新的代碼,從_diagnostics中取出文件關(guān)聯(lián)的診斷結(jié)果,并設置到_currentDiagnostics對象中
          ????????//?觸發(fā)更新
          ????????const?fileDiagnostics?=?this._diagnostics.get(file);
          ????????this._currentDiagnostics.set(file,?fileDiagnostics???fileDiagnostics.getDiagnostics(this._settings)?:?[]);
          ????}

          }

          LSP 客戶端在收到語言服務器的診斷結(jié)果后,調(diào)用 DiagnosticsManager 對象的 updateDiagnostics 方法,診斷結(jié)果就能在 VSCode 上顯示出來了。

          三、TypeScript 與 babel

          在開發(fā)過程中,錯誤提示功能由 VSCode 提供。但是我們的代碼需要經(jīng)過編譯之后才能在瀏覽器中運行,這個過程中是什么東西處理了 TypeScript 呢?答案是 Babel。Babel 最初是設計用來將 ECMAScript 2015+的代碼轉(zhuǎn)換成后向兼容的代碼,主要工作就是語法轉(zhuǎn)換和 polyfill。只要 Babel 能識別 TypeScript 語法,就能對 TypeScript 語法進行轉(zhuǎn)換。因此,Babel 和 TypeScript 團隊進行了長達一年的合作,推出了@babel/preset-typescript 這個插件。使用這個插件,就能將 TypeScript 轉(zhuǎn)換成JavaScript。

          Babel 有兩種常見使用場景,一種是直接在 CLI 中調(diào)用 babel 命令,另一種是將Babel 和打包工具(如 webpack)結(jié)合使用。由于 babel 自身并不具備打包功能,所以直接在命令行中調(diào)用 babel 命令的用處不大,本節(jié)主要討論如何在 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 文件時,會調(diào)用 label-loader 處理這個文件。label-loader 將這個文件轉(zhuǎn)換成標準 JavaScript 文件后,將處理結(jié)果交還 webpack,webpack 繼續(xù)后面的流程。label-loader 是怎么將 TypeScript 文件轉(zhuǎn)換成標準 JavaScript 文件的呢?答案是直接刪除掉類型注解。先看一下 babel 的工作流程,babel 主要有三個處理步驟:解析、轉(zhuǎn)換和生成。

          1. 解析:將原代碼處理為 AST。對應 babel-parse
          2. 轉(zhuǎn)換:對 AST 進行遍歷,在此過程中對節(jié)點進行添加、更新、移除等操作。對應 babel-tranverse。
          3. 生成:把轉(zhuǎn)換后的 AST 轉(zhuǎn)換成字符串形式的代碼,同時創(chuàng)建源碼映射。對應 babel-generator。

          在加入@babel/preset-typescript 之后,babel 這三個步驟是如何運行呢

          1. 解析:調(diào)用 babel-parser 的 typescript 插件,將源代碼處理成 AST。
          2. 轉(zhuǎn)換:babel-tranverse 的過程中會調(diào)用 babel-plugin-transform-typescript 插件,遇到類型注解節(jié)點,直接移除。
          3. 生成:遇到類型注解類型節(jié)點,調(diào)用對應輸出方法。其它如常。

          使用 babel,不僅能處理 typescript,之前 babel 就已經(jīng)存在的 polyfill 功能也能一并享受。并且由于 babel 只是移除類型注解節(jié)點,所以速度相當快。那么問題來了,既然 babel 把類型注解移除了,我們寫 TypeScript 還有什么意義呢?我認為主要有以下幾點考慮:

          1. 性能方面,移除類型注解速度最快。收集類型并且驗證類型是否正確,是一個相當耗時的操作。
          2. babel 本身的限制。本文第一節(jié)分析過,進行類型驗證之前,需要解析項目中所有文件,收集類型信息。而 babel 只是一個單文件處理工具。Webpack 在調(diào)用 loader 處理文件時,也是一個文件一個文件調(diào)用的。所以 babel 想驗證類型也做不到。并且 babel 的三個工作步驟中,并沒有輸出錯誤的功能。
          3. 沒有必要。類型驗證錯誤提示可以交給編輯器。


          當然,由于 babel 的單文件特性,@babel/preset-typescript 對于一些需要收集完整類型系統(tǒng)信息才能正確運行的 TypeScript 語言特性,支持不是很好,如 const enums 等。完整信息可以查看文檔[1]。

          四、TSC

          VSCode 只提示類型錯誤,babel 完全不校驗類型,如果我們想保證提交到代碼倉庫的代碼是類型正確的,應該怎么做呢?這時可以使用 tsc 命令。

          tsc --noEmit --skipLibCheck

          只需要在項目中運行這個命令,就可以對項目代碼進行類型校驗。如果再配合 husky,在 gitcommit 之前先執(zhí)行一下這個命令,檢查一下類型。如果類型驗證不通過就不執(zhí)行 git commit,這樣整個開發(fā)體驗就很完美了。

          tsc 命令對應的 TypeScript 版本,就是 node_modules 下安裝的 TypeScript 的版本,這個版本可能跟 VSCode 的 TypeScript 插件使用的 tsserver 的版本不一致。這在大多數(shù)情況下沒有問題,VSCode 內(nèi)置的 TypeScript 版本一般都比項目中依賴的TypeScript 版本高,TypeScript 是后向兼容的。如果遇到 VSCode 類型檢查正常,但是 tsc 命令檢查出錯,或相反的情況,可以從版本方面排查一下。

          五、總結(jié)

          e068103b6d06e2f05c9ae4f65fbe8638.webp本文探討了 TypeScript 的工作原理,以及幫助 TypeScript 在項目開發(fā)中發(fā)揮作用的工具。希望能給大家一些啟發(fā)。

          附錄
          • 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/#

          瀏覽 49
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕一区在线观看视频 | 欧美淫秽在线视频 | 丁香六月婷婷五月 | 青榴忘忧草 成人网站 | 翔田千里 青青 久久 |