TS 原理詳細解讀(7)綁定1-符號


在上一節主要介紹了語法樹的解析生成。就好比電腦已經聽到了“你真聰明”這句話,現在要讓電腦開始思考這句話的含義——是真聰明還是假聰明。

這是一個非常的復雜的過程,接下來將有連續幾節內容介紹實現原理,本節則主要提前介紹一些相關的概念。

 

符號

在代碼里面,可以定義一個變量、一個函數、或者一個類,這些定義都有一個名字,然后在其它地方可以通過名字引用這個定義。

這些定義統稱為符號(Symbol)(注意和 ES6 里的 Symbol 不相干)。

當在代碼里寫了一個標識符名稱(變量名),這個名稱一定可以解析為某個符號(可能是變量、參數、函數或其它的,可能是用戶自己寫的,也可能系統自帶),如果無法解析,那一定是一個錯誤。

 

一個符號一般和一個聲明節點對應,比如一個變量符號就對應一個 var 聲明節點或 let/const 聲明節點。

 

可能存在一個符號對應多個聲明節點的情況,比如:

class A{}
interface A{}

同名的類和接口只產生一個符號 A,但這個符號擁有兩個聲明。

 

可能存在一個符號沒有源聲明節點的情況,比如:

type T = {[key: string]: any}

T 有無限個成員,每個成員都是沒有源聲明節點的。

 

TS 中符號的定義:

export interface Symbol {
    flags: SymbolFlags;                     // Symbol flags
    escapedName: __String;                  // Name of symbol
    declarations: Declaration[];            // Declarations associated with this symbol
    valueDeclaration: Declaration;          // First value declaration of the symbol
    members?: SymbolTable;                  // Class, interface or object literal instance members
    exports?: SymbolTable;                  // Module exports
    globalExports?: SymbolTable;            // Conditional global UMD exports
    /* @internal */ id?: number;            // Unique id (used to look up SymbolLinks)
    /* @internal */ mergeId?: number;       // Merge id (used to look up merged symbol)
    /* @internal */ parent?: Symbol;        // Parent symbol
    /* @internal */ exportSymbol?: Symbol;  // Exported symbol associated with this symbol
    /* @internal */ nameType?: Type;        // Type associated with a late-bound symbol
    /* @internal */ constEnumOnlyModule?: boolean; // True if module contains only const enums or other modules with only const enums
    /* @internal */ isReferenced?: SymbolFlags; // True if the symbol is referenced elsewhere. Keeps track of the meaning of a reference in case a symbol is both a type parameter and parameter.
    /* @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol?
    /* @internal */ isAssigned?: boolean;   // True if the symbol is a parameter with assignments
    /* @internal */ assignmentDeclarationMembers?: Map<Declaration>; // detected late-bound assignment declarations associated with the symbol
}

其中,declarations 表示關聯的源節點。valueDeclaration 則表示第一個具有值的源節點。

注意兩者都可能為 `undefined`,源碼中之所以沒將它們標上 `?`,主要是因為作者懶(不然代碼需要經常判斷空)。

 

其中,escapedName 表示符號的名稱,名稱本質是字符串,TS 在源碼中有一些內部的特殊符號名稱,這些名稱都以“__”前綴,如果用戶本身就定義了名字帶__的,會被轉義成其它名字,所以 TS 內部將轉義后的名字標記成 __String 類型,運行期間它本質還是字符串,所以不影響性能。

 

作用域

允許定義符號的節點叫作用域(Scope),比如全局范圍(源文件),函數,大括號。

在同一個作用域中,不能定義同名的符號。

作用域是一個樹結構,查找變量時,先在就近的作用域查找,找不到就向外層查找。

 

如圖共四個作用域:

 

 

仔細觀察會發現,if 語句不是一個作用域,但 for 語句卻是。

語言本身就是這么設計的,因為在 for 里面可以聲明變量。

 

流程節點

流程節點是執行流程圖的組成部分。

 

比如 a = b > c && d == 3 ? e : f 的執行順序:

 

由於 JS 是動態語言,變量的類型可能隨執行的流程發生變化,因此在分析時需要知道整個代碼的執行順序。

流程節點就是擁有記錄這個順序的對象。

 

開始流程

開始流程是整個流程圖的根節點。

// FlowStart represents the start of a control flow. For a function expression or arrow
// function, the node property references the function (which in turn has a flowNode
// property for the containing control flow).
export interface FlowStart extends FlowNodeBase {
    node?: FunctionExpression | ArrowFunction | MethodDeclaration;
}

代碼總是從一個函數開始執行的,所以開始流程也會記錄關聯的函數聲明。

 

標簽流程

如果執行的時候出現判斷,則可能從一個流程進入兩個子流程,即流程跳轉,跳轉的目標叫標簽流程:

// FlowLabel represents a junction with multiple possible preceding control flows.
export interface FlowLabel extends FlowNodeBase {
    antecedents: FlowNode[] | undefined;
}
antecedents 中文意思是祖先,其實是代表執行這個流程節點的上一個父流程節點。
比如上圖例子中,編號 5 就是一個標簽流程,其父流程分別是 e 和 f 所屬流程。
 
代碼中的循環也是以標簽流程的方式出現的。

 

縮小類型范圍的流程

理論上,每行節點都可能對變量的類型有影響,比如上圖例子中,e 所在的位置在流程上可以確認 d == 3。

那么 d == 3 就是一種縮小類型范圍的流程,在這個流程節點后面,統一認為 d 就是 3。

TS 目前並不支持將所有的表達式都按縮小類型范圍的流程處理,只支持特定的幾種表達式,甚至有些表達式如果加了括號就不認識。

這主要是基於性能考慮,這樣可以更少創建流程節點。

 

TS 目前支持的流程有:

// FlowAssignment represents a node that assigns a value to a narrowable reference,
// i.e. an identifier or a dotted name that starts with an identifier or 'this'.
export interface FlowAssignment extends FlowNodeBase {
    node: Expression | VariableDeclaration | BindingElement;
    antecedent: FlowNode;
}

export interface FlowCall extends FlowNodeBase {
    node: CallExpression;
    antecedent: FlowNode;
}

// FlowCondition represents a condition that is known to be true or false at the
// node's location in the control flow.
export interface FlowCondition extends FlowNodeBase {
    node: Expression;
    antecedent: FlowNode;
}

export interface FlowSwitchClause extends FlowNodeBase {
    switchStatement: SwitchStatement;
    clauseStart: number;   // Start index of case/default clause range
    clauseEnd: number;     // End index of case/default clause range
    antecedent: FlowNode;
}

// FlowArrayMutation represents a node potentially mutates an array, i.e. an
// operation of the form 'x.push(value)', 'x.unshift(value)' or 'x[n] = value'.
export interface FlowArrayMutation extends FlowNodeBase {
    node: CallExpression | BinaryExpression;
    antecedent: FlowNode;
}

 

finally 流程

try finally 流程稍微有些麻煩。

try {
    // 1
    // ...(其它代碼)...
    // 2
} catch {
    // 3
    // ...(其它代碼)...
    // 4
} finally {
    // 5
}
// 6

對於位置 5,其父流程是 1/2/3/4 (假設 try 中任何一行代碼都可能報錯)

對於位置 6,其父流程只能是 5,且此時的 5 的父流程只能是 2/4,

所以位置 6 的父流程節點是 finally 結束位置的節點 5,而節點 5 的父流程有兩種可能:1/2/3/4 或只有 2/4

所以 TS 在 5 的前后插入了兩個特殊的流程節點:StartFinally  和 EndFinally,

當遍歷到 EndFinally 時,則給 StartFinally 加鎖,說明此時需要的是 finally 之后的流程節點,否則說明需要的是 finally 本身的父節點

export interface AfterFinallyFlow extends FlowNodeBase, FlowLock {
    antecedent: FlowNode;
}

export interface PreFinallyFlow extends FlowNodeBase {
    antecedent: FlowNode;
    lock: FlowLock;
}
  export interface FlowLock {
    locked?: boolean;
  }

 

綜合

以上所有類型的流程節點,通過父節點的方式組成一個圖

export type FlowNode =
        | AfterFinallyFlow
        | PreFinallyFlow
        | FlowStart
        | FlowLabel
        | FlowAssignment
        | FlowCall
        | FlowCondition
        | FlowSwitchClause
        | FlowArrayMutation;

 export interface FlowNodeBase {
    flags: FlowFlags;
    id?: number;     // Node id used by flow type cache in checker
}

如果將流程節點可視化,將類似地鐵圖,每個節點就是一個站點,站點之間的線路存在分叉,也存在合並,也存在循環。 

 

自測題

為了幫助驗證到此為止的知識點是否都已掌握,這里准備了一些題目:

1. 解釋以下術語:

  • Token
  • Node
  • SyntaxKind
  • Symbol
  • Declaration
  • TypeNode
  • FlowNode
  • Increamentable
  • fullStart
  • Trivial

2. 闡述編譯器從源文件到符號的流程步驟

3. 猜測編譯器在生成符號后的操作內容


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM