新聞中心
這篇文章整理了我是怎么讀 Typescript 源碼的,類似的技巧也可以用于其他庫(kù)的源碼閱讀。

南鄭網(wǎng)站制作公司哪家好,找成都創(chuàng)新互聯(lián)!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、響應(yīng)式網(wǎng)站開發(fā)等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營(yíng)維護(hù)。成都創(chuàng)新互聯(lián)成立于2013年到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選成都創(chuàng)新互聯(lián)。
先從一個(gè) ts 的語法開始:
Test
- 如果 checkType(extends 左邊的類型) 是 T,則把聯(lián)合類型拆開后解析類型,最后合并成一個(gè)聯(lián)合類型返回。
- 如果 checkType 不是 T,把聯(lián)合類型整體作為 T 來解析,返回解析后的類型。
這個(gè)語法叫 Distributive Condition Type,分布式條件類型。設(shè)計(jì)的目的就是為了簡(jiǎn)化 Test
這里不談這個(gè)語法設(shè)計(jì)的怎么樣,我們通過這個(gè)語法的實(shí)現(xiàn)作為抓手,來探究一下 ts 源碼應(yīng)該怎么讀。
類型的表示法:類型對(duì)象
ts 會(huì)把源碼進(jìn)行 parse,生成 AST,然后從 AST 中解析出類型信息。
ts 的類型信息是通過類型對(duì)象來存儲(chǔ)的,我們來看幾個(gè)例子。(可視化的查看 AST 可以使用 astexplorer.net 這個(gè)網(wǎng)站。)
上面定義了四個(gè)類型:
a 類型是 LiteralType,字面量類型,literal 屬性保存具體的字面量,這里是 NumericLiteral,數(shù)字字面量。
b 類型是 UnionType,聯(lián)合類型,types 屬性保存了它所包含的類型,這里是兩個(gè) LiteralType
T extends boolean 這部分是一個(gè) ConditionType,有 checkType、extendsType、trueType、falseType 四個(gè)屬性分別代表不同的部分。
可以看到,T 是一個(gè) TypeReference 類型,也就是它只是一個(gè)變量引用,具體的值還是泛型參數(shù)傳入的類型。
Test
所以說,類型在 ts 里面都是通過類型對(duì)象來表示的。
比較特別的是 TypeReference 類型,它只是一個(gè)引用,具體的類型還得把類型參數(shù)傳入所引用的類型,然后求出最終類型。比如這里的 Test
理解了類型是怎么表示的,高級(jí)類型和泛型參數(shù)都是什么,接下來我們就可以正式通過調(diào)試 ts 源碼來看下 ConditionType 的解析過程了。
VSCode 調(diào)試 Typescript 源碼
首先,我們要把 ts 源碼下載下來(加個(gè) depth=1 可以下載單 commit,速度比較快)
- git clone --depth=1 git@github.com:microsoft/TypeScript.git
然后可以看到 lib 目錄下有 tsc.js 和 typescript.js,這兩個(gè)分別是 ts 的命令行和 api 的入口。
但是,這些是編譯以后的 js 代碼,源碼在 src 下,是用 ts 寫的。
怎么把編譯后的 js 代碼和 ts 源碼關(guān)聯(lián)起來呢?sourcemap!
默認(rèn)編譯出來的產(chǎn)物是沒有 sourcemap 的,我們要修改下編譯配置:
修改下 src/tsconfig-library-base.json,(這是 ts 生成 lib 代碼的編譯配置)把 sourceMap 改為 true。
之后再編譯源碼:
- yarn
- yarn run build:compiler
然后就可以看到多了一個(gè) built 目錄,下面有 tsc.js、typescript.js 這兩個(gè)入口文件,而且也有了 sourcemap:
接下來就可以直接調(diào)試 ts 源碼,而不是編譯后的 js 代碼了。信么?
不信我們來試試。
vscode 直接調(diào)試 ts
vscode 在項(xiàng)目根目錄下的 .vscode/launch.json 下保存調(diào)試配置:
我們添加一個(gè)調(diào)試配置:
- {
- "name": "調(diào)試 ts 源碼",
- "program": "${workspaceFolder}/built/local/tsc.js",
- "request": "launch",
- "skipFiles": [
- "
/**" - ],
- "args": [
- "./input.ts"
- ],
- "stopOnEntry": true,
- "type": "node"
- }
含義如下:
- name:調(diào)試配置的名字
- program:調(diào)試的目標(biāo)程序的地址
- request:有 launch 和 attch 兩個(gè)取值,代表啟動(dòng)新的還是連上已有的
- skipFiles:調(diào)試的時(shí)候跳過一些文件,這里配置的是跳過 node 內(nèi)部的那些文件,調(diào)用棧會(huì)更簡(jiǎn)潔
- args:命令行參數(shù)
- stopOnEntry:是否在首行加個(gè)斷點(diǎn)
- type:調(diào)試的類型,這里是用 node 來跑
保存之后就可以在調(diào)試面板看到該調(diào)試選項(xiàng):
這里我們?cè)O(shè)計(jì)的 input.ts 是這樣的:
- type Test
= T extends boolean ? "Y" : "N"; - type res = Test
;
在 ts 的 checker.ts 部分打個(gè)斷點(diǎn),然后點(diǎn)擊啟動(dòng)調(diào)試。
然后,看,這斷住的地方,就是 ts 源碼啊,不是編譯后的 js 文件。這就是 sourcemap 的作用。
還可以在左邊文件樹看到源碼的目錄結(jié)構(gòu),這比調(diào)試編譯后的 js 代碼爽多了。
會(huì)了通過 sourcemap 調(diào)試源碼之后,我們?cè)撨M(jìn)入主題了:通過源碼探究分布式條件類型的實(shí)現(xiàn)原理。
其實(shí)我們上面使用的是 tsc.js 的命令行入口來調(diào)試的,這樣其實(shí)代碼比較多,很難理清要看哪部分代碼。怎么辦呢?
接下來就是我的秘密武器了,用 typescript compiler api。
typescript compiler api
ts 除了命令行工具的入口外,也提供了 api 的形式,只是我們很少用。但它對(duì)于探究 ts 源碼實(shí)現(xiàn)有很大的幫助。
我們定義個(gè) test.js 文件,引入 typescript 的包:
- const ts = require("./built/local/typescript");
然后用 ts 的 api 傳入編譯配置,并 parse 源碼成 ast:
- const filename = "./input.ts";
- const program = ts.createProgram([filename], {
- allowJs: false
- });
- const sourceFile = program.getSourceFile(filename);
這里的 createProgram 第二個(gè)參數(shù)是編譯配置,我傳入了個(gè) allowJS 意思了一下。
program.getSourceFile 返回的就是 ts 的 AST。
并且還可以拿到 typeChecker:
- const typeChecker = program.getTypeChecker();
然后呢?typeChecker 是類型檢查的 api,我們可以遍歷 AST 找到檢查的 node,然后調(diào)用 checker 的 api 進(jìn)行檢查:
- function visitNode(node) {
- if (node.kind === ts.SyntaxKind.TypeReference) {
- const type = typeChecker.getTypeFromTypeNode(node);
- debugger;
- }
- node.forEachChild(child =>
- visitNode(child)
- );
- }
- visitNode(sourceFile);
我們判斷了如果 AST 是 TypeReference 類型,則用 typeChecker.getTypeFromTypeNode 來解析類型。
接下來就可以精準(zhǔn)的調(diào)試該類型解析的邏輯了,相比命令行的方式來說,更方便理清邏輯。
完整代碼如下:
- const ts = require("./built/local/typescript");
- const filename = "./input.ts";
- const program = ts.createProgram([filename], {
- allowJs: false
- });
- const sourceFile = program.getSourceFile(filename);
- const typeChecker = program.getTypeChecker();
- function visitNode(node) {
- if (node.kind === ts.SyntaxKind.TypeReference) {
- const type = typeChecker.getTypeFromTypeNode(node);
- debugger;
- }
- node.forEachChild(child =>
- visitNode(child)
- );
- }
- visitNode(sourceFile);
我們改下調(diào)試配置,然后開始調(diào)試:
- {
- "name": "調(diào)試 ts 源碼",
- "program": "${workspaceFolder}/test.js",
- "request": "launch",
- "skipFiles": [
- "
/**" - ],
- "args": [
- ],
- "type": "node"
- }
在 typeChecker.getTypeFromTypeNode 這行打個(gè)斷點(diǎn),我們?nèi)タ聪戮唧w的類型解析過程。
然后,XDM,打起精神,本文的高潮部分來了:
我們進(jìn)入了 getTypeFromTypeNode 方法,這個(gè)方法就是根據(jù) AST 的類型來做不同的解析,返回類型對(duì)象的。各種類型解析的邏輯都是從這里進(jìn)入的,這是一個(gè)重要的交通樞紐。
然后我們進(jìn)入了 TypeReference 的分支,因?yàn)?Test
TypeReference 的類型就是它引用的類型,它引用了 ConditionType,所以會(huì)再解析 T extends boolean 這個(gè) ConditionType 的類型:
所有的類型都是按照 ast node 的 id 存入一個(gè) nodeLinks 的 map 中來緩存,只有第一次需要解析,之后直接拿結(jié)果。比如上圖的 resolvedType 就存入了 nodeLinks 來緩存。
然后,XDM,看到閃閃發(fā)光的那行代碼了么?
解析 ConditionType 的類型的時(shí)候會(huì)根據(jù) checkType 部分是否是類型參數(shù)(TypeParameter,也就是泛型)來設(shè)置 isDistributive 屬性。
之后解析 TypeReference 類型的時(shí)候,會(huì)傳入具體的類型來實(shí)例化:
這里就判斷了 conditionType 的 isDistributive 屬性,如果是,則把 unionType 的每個(gè)類型分別傳入來解析,最后合并返回。
如圖,我們走到了 isDistributive 為 true 的這個(gè)分支。
那么解析出的類型就是 'Y' | 'N' 的聯(lián)合類型。
那我們把 input.ts 代碼改一下呢:
- type Test
= [T] extends [boolean] ? "Y" : "N"; - type res = Test
;
checkType 不直接寫類型參數(shù) T 了。
再跑一次:
這次沒進(jìn)去了。
難道說?
確實(shí),這樣的結(jié)果就是 N。
說明了什么?說明了 ConditionType 是根據(jù) checkType 是否是類型參數(shù)來設(shè)置了 isDistributive 屬性,之后解析 TypeReference 的時(shí)候根據(jù) isDistributive 的值分別做了不同的解析。
那么只要 checkType 不是 T 就行了。
所以這樣也行:
這樣也行:
我們經(jīng)常用 [T] 來避免 distributive 只不過這樣比較簡(jiǎn)潔,看完源碼我們知道了,其實(shí)別的方式也行。
就這樣,我們通過源碼理清了這個(gè)語法的實(shí)現(xiàn)原理。
總結(jié)
我們以探究 distributive condition type 的實(shí)現(xiàn)原理為目的來閱讀了 typescript 源碼。
首先把 typescript 源碼下載下來,然后改下編譯配置,生成帶有 sourcemap 的代碼,之后在 vscode 里調(diào)試,這樣可以直接調(diào)試編譯前的源碼,信息更多。
typescript 有 cli 和 api 兩種入口,用 cli 的方式無關(guān)代碼太多,比較難理清,所以我們用 api 的方式來寫了一段測(cè)試代碼,之后打斷點(diǎn)來調(diào)試。
ts 的類型信息保存在類型對(duì)象中,這個(gè)可以用 astexplorer.net 來可視化的查看。
用 typeChecker.getTypeFromTypeNode 可以拿到某個(gè)類型的具體值,我們就是通過這個(gè)作為入口來探究各種類型的解析邏輯。
源碼中比較重要的有這么幾點(diǎn):
- getTypeFromTypeNode 方法是通過 node 獲取類型的入口方法,所有 AST 的類型對(duì)象都是通過這個(gè)方法拿到
- nodeLinks 保存了解析后的類型,key 為 node id,這樣解析一遍就好了,下次拿緩存。
之后我們看了 ConditionType 的解析邏輯會(huì)根據(jù) checkType 是否為類型參數(shù)來設(shè)置 isDistributive 屬性,然后 TypeReference 實(shí)例化該類型的時(shí)候會(huì)根據(jù) isDistributive 的值進(jìn)入不同的處理邏輯,這就是它的實(shí)現(xiàn)原理。
理解了原理之后,我們?cè)偈褂?distributive condition type 就心里有底了,還可以創(chuàng)造很多變形使用,不局限于 [T]。
本文以調(diào)試一個(gè)類型解析邏輯的原理為抓手探究了 ts 源碼閱讀方式,調(diào)試 ts 別的部分的代碼,或者調(diào)試其他的庫(kù)也是類似的。
希望可以幫助大家掌握 typescript 源碼調(diào)試技巧,想探究某個(gè)類型語法實(shí)現(xiàn)原理的時(shí)候,可以通過源碼層面來徹底搞清楚。源碼面前,沒有秘密。
當(dāng)前題目:我讀Typescript源碼的秘訣都在這里了
鏈接URL:http://www.dlmjj.cn/article/cdipdse.html


咨詢
建站咨詢
