Vue设计与实现 笔记 编译器

125 阅读4分钟

Vue设计与实现 笔记 编译器

编译核心技术概览

工作流程:

  1. 分析模板,将其解析为模板AST
  2. 将模板AST转换为用于描述渲染函数的JavaScript AST
  3. 根据JavaScript AST生成渲染函数代码

完整的编译过程通常包括:

  1. 词法分析
  2. 语法分析
  3. 语义分析
  4. 中间代码生成
  5. 优化
  6. 目标代码生成

Vue模板编译器的工作流程

<div>
  <h1 v-if="ok">Vue Template</h1>
</div>

编译成:

const ast = {
  // 逻辑根节点
  type: 'Root',
  children: [
  // div 标签节点
    {
      type: 'Element',
      tag: 'div'
      children: [
        // h1 标签节点
        {
          type: 'Element',
          tag: 'h1',
          props: [
            // v-if 指令节点
            {
              type: 'Directive', 
              name: 'if',
              exp: {
                // 表达式节点
                type: 'Expression',
                content: 'ok'
              }
            }
          ]
        }
      ]
    }
  ]
}

模板 AST 具有与模板同构的嵌套结构。每一棵 AST 都有一个逻辑上的根节点,其类型为 Root。模板中真正的根节点则作为 Root 节点的 children 存在。

  • 不同类型的节点是通过节点的 type 属性进行区分的。例如标签节点的 type 值为 'Element'。
  • 标签节点的子节点存储在其 children 数组中。
  • 标签节点的属性节点和指令节点会存储在 props 数组中。
  • 不同类型的节点会使用不同的对象属性进行描述。例如指令节点拥有 name 属性,用来表达指令的名称,而表达式节点拥有content 属性,用来描述表达式的内容。

封装parse函数来完成对模板的词法分析和语法分析,得到AST模板。

parse的实现原理与状态机

函数入参为字符串模板,解析器会逐个读取字符串的字符,并将整个字符串切割为一个个Token(这里的Token可以视作词法记号)

(正则表达式本质上就是有限自动机)

构造AST

根据Token列表构建AST的过程,其实就是对Token列表进行扫描的过程

每遇到一个开始标签节点,我们就将其压入栈中,遇到结束节点,就将其从栈中弹出。扫描过程中遇到的所有子节点,都会作为当前栈顶节点的子节点,并添加到栈顶节点的children属性下。

AST的转换与插件化构造

需要一个深度优先遍历算法,实现对AST中节点的访问

更加理想的转换工作流更加理想的转换工作流

增加一个数组 exitFns,用来存储由转换函数返回的回调函数。接着,在 traverseNode 函数的最后,执行这些缓存在 exitFns 数组中的回调函数。这样就保证了,当退出阶段的回调函数执行时,当前访问的节点的子节点已经全部处理过了。

将模板AST转化为JavaScript AST

为了把AST转化为JavaScript AST,我们需要两个转换函数,transformElementtransformText,它们分别用来处理标签节点和文本节点。

  • 转换文本节点:文本节点对应的JavaScript AST节点就是一个字符串常量,只需要使用node.content创建一个StringLiteral类型的节点即可,最后将文本节点对应的JavaScript AST节点添加到node.jsNode属性下。
  • 转换标签节点:要将转换逻辑编写在退出阶段的回调函数内,才能包装其子节点都被处理完毕。
  • 转换根节点:根节点的第一个子节点就是模板的根节点,创建render函数声明语句节点,将vnodeJSAST作为render函数体返回语句。

代码生成

上下文对象:用来维护代码生成过程中程序的运行状态。

  • 对于 FunctionDecl 节点,使用 genFunctionDecl 函数为该类型节点生成对应的 JavaScript 代码。
  • 对于 ReturnStatement 节点,使用 genReturnStatement 函数为该类型节点生成对应的 JavaScript 代码。
  • 对于 CallExpression 节点,使用 genCallExpression 函数为该类型节点生成对应的 JavaScript 代码。
  • 对于 StringLiteral 节点,使用 genStringLiteral 函数为该类型节点生成对应的 JavaScript 代码。
  • 对于 ArrayExpression 节点,使用 genArrayExpression 函数为该类型节点生成对应的 JavaScript 代码。

总结:代码生成的过程就是字符串拼接的过程。需要为不同的 AST 节点编写对应的代码生成函数。为了让生成的代码具有更强的可读性,我们还讨论了如何对生成的代码进行缩进和换行。我们将用于缩进和换行的代码封装为工具函数,并且定义到代码生成过程中的上下文对象中。