1. 概览

大多数的 JavaScript 转译器(transpiler) 都比 Typescript 简单 因为它们几乎没提供代码分析的方法。
典型的 JavaScript 转换器 只有以下流程

  1. 源码 ~~ 扫描器 ~~ Tokens ~~ 解析器 ~~ AST ~~ 发射器 JavaScript

上述架构确实对于 简化 Typescript生成 Javascript 的理解有帮助 但缺少了一个关键功能 即 Typescript 的语义系统。 为了协助(检查器)类型检查。 绑定器将源码的各部分连接成一个相关的类型系统, 供检查器使用。

绑定器的主要职责是创建符号(Symbol)

2. 符号

此时又引入一个新的概念 那就是符号。 我们先看下官方的解析 符号将AST的声明节点 与 其他声明连接到相同的实体上。 符号是语义系统的基本构造块。 符号的构造器定义在 core.ts

(绑定器实际上通过 objectAllocator.getSymbolConstructor 来获取构造器)

  1. function Symbol(this: Symbol, flags: SymbolFlags, name: string) {
  2. this.flags = flags;
  3. this.name = name;
  4. this.declarations = undefined;
  5. }

我们先看 对于 Symbol 的定义

  1. export interface Symbol {
  2. flags: SymbolFlags; // Symbol flags
  3. name: string; // Name of symbol
  4. declarations?: Declaration[]; // Declarations associated with this symbol
  5. valueDeclaration?: Declaration; // First value declaration of the symbol
  6. members?: SymbolTable; // Class, interface or literal instance members
  7. exports?: SymbolTable; // Module exports
  8. globalExports?: SymbolTable; // Conditional global UMD exports
  9. /* @internal */ id?: number; // Unique id (used to look up SymbolLinks)
  10. /* @internal */ mergeId?: number; // Merge id (used to look up merged symbol)
  11. /* @internal */ parent?: Symbol; // Parent symbol
  12. /* @internal */ exportSymbol?: Symbol; // Exported symbol associated with this symbol
  13. /* @internal */ constEnumOnlyModule?: boolean; // True if module contains only const enums or other modules with only const enums
  14. /* @internal */ isReferenced?: boolean; // True if the symbol is referenced elsewhere
  15. /* @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol?
  16. /* @internal */ isAssigned?: boolean; // True if the symbol is a parameter with assignments
  17. }

其次 还有 SymbolFlags 的定义 符号标志是个标志枚举,用于识别额外的符号类别(例如:变量作用域标志 FunctionScopedVariable 或 BlockScopedVariable 等)

  1. export const enum SymbolFlags {
  2. None = 0,
  3. FunctionScopedVariable = 1 << 0, // Variable (var) or parameter
  4. BlockScopedVariable = 1 << 1, // A block-scoped variable (let or const)
  5. Property = 1 << 2, // Property or enum member
  6. EnumMember = 1 << 3, // Enum member
  7. Function = 1 << 4, // Function
  8. Class = 1 << 5, // Class
  9. Interface = 1 << 6, // Interface
  10. ConstEnum = 1 << 7, // Const enum
  11. RegularEnum = 1 << 8, // Enum
  12. ValueModule = 1 << 9, // Instantiated module
  13. NamespaceModule = 1 << 10, // Uninstantiated module
  14. TypeLiteral = 1 << 11, // Type Literal or mapped type
  15. ObjectLiteral = 1 << 12, // Object Literal
  16. Method = 1 << 13, // Method
  17. Constructor = 1 << 14, // Constructor
  18. GetAccessor = 1 << 15, // Get accessor
  19. SetAccessor = 1 << 16, // Set accessor
  20. Signature = 1 << 17, // Call, construct, or index signature
  21. TypeParameter = 1 << 18, // Type parameter
  22. TypeAlias = 1 << 19, // Type alias
  23. ExportValue = 1 << 20, // Exported value marker (see comment in declareModuleMember in binder)
  24. ExportType = 1 << 21, // Exported type marker (see comment in declareModuleMember in binder)
  25. ExportNamespace = 1 << 22, // Exported namespace marker (see comment in declareModuleMember in binder)
  26. Alias = 1 << 23, // An alias for another symbol (see comment in isAliasSymbolDeclaration in checker)
  27. Prototype = 1 << 24, // Prototype property (no source representation)
  28. ExportStar = 1 << 25, // Export * declaration
  29. Optional = 1 << 26, // Optional property
  30. Transient = 1 << 27, // Transient symbol (created during type check)
  31. Enum = RegularEnum | ConstEnum,
  32. Variable = FunctionScopedVariable | BlockScopedVariable,
  33. Value = Variable | Property | EnumMember | Function | Class | Enum | ValueModule | Method | GetAccessor | SetAccessor,
  34. Type = Class | Interface | Enum | EnumMember | TypeLiteral | ObjectLiteral | TypeParameter | TypeAlias,
  35. Namespace = ValueModule | NamespaceModule | Enum,
  36. Module = ValueModule | NamespaceModule,
  37. Accessor = GetAccessor | SetAccessor,
  38. // Variables can be redeclared, but can not redeclare a block-scoped declaration with the
  39. // same name, or any other value that is not a variable, e.g. ValueModule or Class
  40. FunctionScopedVariableExcludes = Value & ~FunctionScopedVariable,
  41. // Block-scoped declarations are not allowed to be re-declared
  42. // they can not merge with anything in the value space
  43. BlockScopedVariableExcludes = Value,
  44. ParameterExcludes = Value,
  45. PropertyExcludes = None,
  46. EnumMemberExcludes = Value | Type,
  47. FunctionExcludes = Value & ~(Function | ValueModule),
  48. ClassExcludes = (Value | Type) & ~(ValueModule | Interface), // class-interface mergability done in checker.ts
  49. InterfaceExcludes = Type & ~(Interface | Class),
  50. RegularEnumExcludes = (Value | Type) & ~(RegularEnum | ValueModule), // regular enums merge only with regular enums and modules
  51. ConstEnumExcludes = (Value | Type) & ~ConstEnum, // const enums merge only with const enums
  52. ValueModuleExcludes = Value & ~(Function | Class | RegularEnum | ValueModule),
  53. NamespaceModuleExcludes = 0,
  54. MethodExcludes = Value & ~Method,
  55. GetAccessorExcludes = Value & ~SetAccessor,
  56. SetAccessorExcludes = Value & ~GetAccessor,
  57. TypeParameterExcludes = Type & ~TypeParameter,
  58. TypeAliasExcludes = Type,
  59. AliasExcludes = Alias,
  60. ModuleMember = Variable | Function | Class | Interface | Enum | Module | TypeAlias | Alias,
  61. ExportHasLocal = Function | Class | Enum | ValueModule,
  62. HasExports = Class | Enum | Module,
  63. HasMembers = Class | Interface | TypeLiteral | ObjectLiteral,
  64. BlockScoped = BlockScopedVariable | Class | Enum,
  65. PropertyOrAccessor = Property | Accessor,
  66. Export = ExportNamespace | ExportType | ExportValue,
  67. ClassMember = Method | Accessor | Property,
  68. /* @internal */
  69. // The set of things we consider semantically classifiable. Used to speed up the LS during
  70. // classification.
  71. Classifiable = Class | Enum | TypeAlias | Interface | TypeParameter | Module,
  72. }

3. 检查器 对 绑定器的使用

实际上 绑定器在检查器 内部调用 而检查器又被程序调用。 简化的调用栈如下所示

  1. program.getTypeChecker ->
  2. ts.createTypeChecker(检查器中)->
  3. initializeTypeChecker(检查器中) ->
  4. for each SourceFile `ts.bindSourceFile`(绑定器中)
  5. // followed by
  6. for each SourceFile `ts.mergeSymbolTable`(检查器中)

而 SourceFile 是绑定器的工作单元 binder.ts 由 checker.ts 驱动

bindSourceFile 和 mergeSymbolTable 是 两个关键的绑定器函数

3.1 bindSourceFile

  1. export function bindSourceFile(file: SourceFile, options: CompilerOptions) {
  2. performance.mark("beforeBind");
  3. binder(file, options);
  4. performance.mark("afterBind");
  5. performance.measure("Bind", "beforeBind", "afterBind");
  6. }

这里的 performance 相关的函数 我们可以先 忽略 这里 可以看到 bindSourceFile 的参数 是 SourceFile 主要执行的 是 binder 方法 那我们继续往下看

3.2 binder

  1. const binder = createBinder();
  2. function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
  3. function bindSourceFile(f: SourceFile, opts: CompilerOptions) {
  4. file = f;
  5. options = opts;
  6. languageVersion = getEmitScriptTarget(options);
  7. inStrictMode = bindInStrictMode(file, opts);
  8. classifiableNames = createMap<string>();
  9. symbolCount = 0;
  10. skipTransformFlagAggregation = file.isDeclarationFile;
  11. Symbol = objectAllocator.getSymbolConstructor();
  12. if (!file.locals) {
  13. bind(file);
  14. file.symbolCount = symbolCount;
  15. file.classifiableNames = classifiableNames;
  16. }
  17. // ...
  18. }
  19. return bindSourceFile;
  20. // ...
  21. }

我们可以看到 这里的 binder 的执行 最终步骤 还是 执行的 bindSourceFile 方法。

该函数主要是检查 file.locals 是否定义 如果 没有 则交给 bind 来处理。

  1. /* @internal */ locals?: SymbolTable; // Locals associated with node (initialized by binding)

注意: locals 定义在节点上 其 类型是 SymbolTable。 SourceFile 也是一个节点(事实上也是 AST 中的根节点)

而一开始的 file 肯定是没有被定义的 因为我们可以往下走 bind 的逻辑

3.3 bind

  1. function bind(node: Node): void {
  2. if (!node) {
  3. return;
  4. }
  5. node.parent = parent;
  6. const saveInStrictMode = inStrictMode;
  7. // Even though in the AST the jsdoc @typedef node belongs to the current node,
  8. // its symbol might be in the same scope with the current node's symbol. Consider:
  9. //
  10. // /** @typedef {string | number} MyType */
  11. // function foo();
  12. //
  13. // Here the current node is "foo", which is a container, but the scope of "MyType" should
  14. // not be inside "foo". Therefore we always bind @typedef before bind the parent node,
  15. // and skip binding this tag later when binding all the other jsdoc tags.
  16. if (isInJavaScriptFile(node)) bindJSDocTypedefTagIfAny(node);
  17. // First we bind declaration nodes to a symbol if possible. We'll both create a symbol
  18. // and then potentially add the symbol to an appropriate symbol table. Possible
  19. // destination symbol tables are:
  20. //
  21. // 1) The 'exports' table of the current container's symbol.
  22. // 2) The 'members' table of the current container's symbol.
  23. // 3) The 'locals' table of the current container.
  24. //
  25. // However, not all symbols will end up in any of these tables. 'Anonymous' symbols
  26. // (like TypeLiterals for example) will not be put in any table.
  27. bindWorker(node);
  28. // Then we recurse into the children of the node to bind them as well. For certain
  29. // symbols we do specialized work when we recurse. For example, we'll keep track of
  30. // the current 'container' node when it changes. This helps us know which symbol table
  31. // a local should go into for example. Since terminal nodes are known not to have
  32. // children, as an optimization we don't process those.
  33. if (node.kind > SyntaxKind.LastToken) {
  34. const saveParent = parent;
  35. parent = node;
  36. const containerFlags = getContainerFlags(node);
  37. if (containerFlags === ContainerFlags.None) {
  38. bindChildren(node);
  39. }
  40. else {
  41. bindContainer(node, containerFlags);
  42. }
  43. parent = saveParent;
  44. }
  45. else if (!skipTransformFlagAggregation && (node.transformFlags & TransformFlags.HasComputedFlags) === 0) {
  46. subtreeTransformFlags |= computeTransformFlagsForNode(node, 0);
  47. }
  48. inStrictMode = saveInStrictMode;
  49. }
  1. 它为当前节点 添加一个 parent
  2. 调用 bindWorker 根据不同的节点调用与之对应的绑定函数
  3. 调用bindChildren对 当前节点 的每个子节点进行一一绑定 bindChildren 内部也是通过递归调用bind 对每一个节点进行一一绑定

3.4 bindWorker

  1. function bindWorker(node: Node) {
  2. switch (node.kind) {
  3. /* Strict mode checks */
  4. case SyntaxKind.Identifier:
  5. // for typedef type names with namespaces, bind the new jsdoc type symbol here
  6. // because it requires all containing namespaces to be in effect, namely the
  7. // current "blockScopeContainer" needs to be set to its immediate namespace parent.
  8. if ((<Identifier>node).isInJSDocNamespace) {
  9. let parentNode = node.parent;
  10. while (parentNode && parentNode.kind !== SyntaxKind.JSDocTypedefTag) {
  11. parentNode = parentNode.parent;
  12. }
  13. bindBlockScopedDeclaration(<Declaration>parentNode, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes);
  14. break;
  15. }
  16. // ....
  17. }
  18. }

单论截取的这段代码 我们可以看出 bindWork 里面做的事情是根据 node.kind (SyntaxKind类型) 进行分别绑定,并且将工作委托交给 bindXXX 函数进行实际的绑定操作。

下面就以 Identifier 作为例子 我们看下 bindBlockScopedDeclaration 做了什么

3.5 bindBlockScopedDeclaration

  1. function bindBlockScopedDeclaration(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) {
  2. switch (blockScopeContainer.kind) {
  3. case SyntaxKind.ModuleDeclaration:
  4. declareModuleMember(node, symbolFlags, symbolExcludes);
  5. break;
  6. case SyntaxKind.SourceFile:
  7. if (isExternalModule(<SourceFile>container)) {
  8. declareModuleMember(node, symbolFlags, symbolExcludes);
  9. break;
  10. }
  11. // falls through
  12. default:
  13. if (!blockScopeContainer.locals) {
  14. blockScopeContainer.locals = createMap<Symbol>();
  15. addToContainerChain(blockScopeContainer);
  16. }
  17. declareSymbol(blockScopeContainer.locals, /*parent*/ undefined, node, symbolFlags, symbolExcludes);
  18. }
  19. }

我们最后发现 基本上大多数的定义都会走到 declareSymbol 函数 当然 其他 declareModuleMember 其实最终也会走到 declareSymbol 来定义符号

3.6 declareSymbol

  1. function declareSymbol(symbolTable: SymbolTable, parent: Symbol, node: Declaration, includes: SymbolFlags, excludes: SymbolFlags): Symbol {
  2. Debug.assert(!hasDynamicName(node));
  3. const isDefaultExport = hasModifier(node, ModifierFlags.Default);
  4. // The exported symbol for an export default function/class node is always named "default"
  5. const name = isDefaultExport && parent ? "default" : getDeclarationName(node);
  6. let symbol: Symbol;
  7. if (name === undefined) {
  8. symbol = createSymbol(SymbolFlags.None, "__missing");
  9. }
  10. else {
  11. symbol = symbolTable.get(name);
  12. addDeclarationToSymbol(symbol, node, includes);
  13. symbol.parent = parent;
  14. // ..
  15. return symbol;
  16. }

从上面描述的来看 declareSymbol 其实主要做了两件事情

  1. createSymbol
  2. addDeclarationToSymbol

createSymbol

  1. function createSymbol(flags: SymbolFlags, name: string): Symbol {
  2. symbolCount++;
  3. return new Symbol(flags, name);
  4. }

createSymbol主要是简单地更新 symbolCount(一个 bindSourceFile 的本地变量),并使用指定的参数创建符号。创建了符号之后需要进行对节点的绑定。

addDeclarationToSymbol

  1. function addDeclarationToSymbol(symbol: Symbol, node: Declaration, symbolFlags: SymbolFlags) {
  2. symbol.flags |= symbolFlags;
  3. node.symbol = symbol;
  4. if (!symbol.declarations) {
  5. symbol.declarations = [];
  6. }
  7. symbol.declarations.push(node);
  8. if (symbolFlags & SymbolFlags.HasExports && !symbol.exports) {
  9. symbol.exports = createMap<Symbol>();
  10. }
  11. if (symbolFlags & SymbolFlags.HasMembers && !symbol.members) {
  12. symbol.members = createMap<Symbol>();
  13. }
  14. if (symbolFlags & SymbolFlags.Value) {
  15. const valueDeclaration = symbol.valueDeclaration;
  16. if (!valueDeclaration ||
  17. (valueDeclaration.kind !== node.kind && valueDeclaration.kind === SyntaxKind.ModuleDeclaration)) {
  18. // other kinds of value declarations take precedence over modules
  19. symbol.valueDeclaration = node;
  20. }
  21. }
  22. }

addDeclarationToSymbol函数内主要做了两件事情:
1. 创建 AST 节点到 symbol 的连接 ( node.symbol = symbol;)
2. 为节点添加一个声明(symbol.declarations.push(node); )。

4. 整体流程

image.png