vue模版编译原理

5 阅读9分钟

Vue 模板编译原理解析 - 完整版

目录


1. 模板编译流程

1.1 编译的定义

编译是指:将一种语言 A(source code,源代码)翻译成另外一种语言 B(target code,目标代码)的过程。

1.2 完整的编译过程

传统的编译流程通常包括以下六个步骤:

源代码
  ↓ 1. 词法解析
Tokens(词法单元)
  ↓ 2. 语法解析
AST(抽象语法树)
  ↓ 3. 语义解析
优化后的 AST
  ↓ 4. 中间代码生成
中间代码
  ↓ 5. 优化
优化后的中间代码
  ↓ 6. 目标代码生成
目标代码
词法解析示例

const i = 5; 为例,解析后的 token:

const(关键字)  i(变量)  =(运算符)  5(操作数)

1.3 Vue 模板编译

在 Vue 中,模板编译指的是将 Vue 模板转换为渲染函数的过程。

输入(source code - 模板):

<div :id="someId">
  <h1>Hello</h1>
</div>

输出(target code - 渲染函数):

function render() {
  return h('div', { id: someId }, [
    h('h1', 'Hello')
  ])
}

1.4 Vue 编译器三大部分

模板
  ↓ 解析器
模板 AST
  ↓ 转换器
JS AST
  ↓ 生成器
渲染函数
组件输入输出核心作用
解析器模板字符串模板 AST根据模板生成对应的模板 AST
转换器模板 ASTJS AST将模板 AST 转换为 JavaScript AST
生成器JS AST渲染函数代码根据 JS AST 生成最终的目标代码

2. 解析器

解析器的核心作用是:负责将模板解析为所对应的 AST

用户所书写的模板:

<p>Vue</p>

对于解析器来讲就是一段字符串:

'<p>Vue</p>'

2.1 有限状态机(FSM)

概念介绍

FSM(Finite State Machine,有限状态机) 是一种数学计算模型,它包含以下基本要素:

  1. 状态:当前系统处于什么状态,系统只能有一种状态
  2. 事件:通过事件从一种状态转换为另一种状态
  3. 转移:从一个状态转换到另一个状态的过程
  4. 初始状态:状态机开始时的状态
  5. 终止状态:状态机结束时的状态

状态迁移示例

以解析字符串 '<p>Vue</p>' 为例:

输入:

'<p>Vue</p>'

状态迁移过程:

字符当前状态转换后状态
<初始状态标签开始状态
p标签开始状态标签名称状态
>标签名称状态初始状态
V初始状态文本状态
u文本状态文本状态
e文本状态文本状态
<文本状态标签开始状态
/标签开始状态标签结束状态
p标签结束状态标签结束名称状态
>标签结束名称状态初始状态

浏览器引擎内部在进行 HTML 解析的时候,也是通过有限状态机的方式来进行解析的。

详细的规范可以参考:HTML 规范 13.2.5.1

输出示例

输入:

'<p>Vue</p>'

输出(Tokens):

[
  { type: 'tag', name: 'p' },      // 开始标签
  { type: 'text', content: 'Vue' }, // 文本节点
  { type: 'tagEnd', name: 'p' }     // 结束标签
]

2.2 Token 解析实现

状态定义

// 首先定义一些状态
const State = {
  initial: 1,        // 初始状态
  tagOpen: 2,        // 标签开始状态
  tagName: 3,        // 标签名称开始状态
  text: 4,           // 文本状态
  tagEnd: 5,         // 标签结束状态
  tagEndName: 6      // 标签名称结束状态
}

Tokenize 函数实现

/**
 * 将模板字符串解析为 tokens 数组
 * @param {string} str - 模板字符串
 * @returns {Array} - tokens 数组
 */
function tokenize(str) {
  let currentState = State.initial; // 初始状态
  const chars = [];                  // 用于存储字符
  const tokens = [];                  // 用于存储 token
  
  while (str) {
    const char = str[0]; // 取第一个字符
    
    // 根据不同的状态处理字符
    switch (currentState) {
      case State.initial:
        // 检查字符
        if (char === "<") {
          currentState = State.tagOpen; // 切换为标签开始状态
          str = str.slice(1);          // 消费一个字符
        } else if (isAlpha(char)) {
          currentState = State.text;    // 进入文本状态
          chars.push(char);             // 将字符存储到 chars 中
          str = str.slice(1);          // 消费一个字符
        }
        break;
        
      case State.tagOpen:
        // 标签开启状态:检查当前字符
        if (isAlpha(char)) {
          currentState = State.tagName; // 进入标签名称状态
          chars.push(char);             // 将字符添加到 chars 数组
          str = str.slice(1);           // 移除已处理的字符
        } else if (char === "/") {
          currentState = State.tagEnd;  // 进入结束标签开始状态
          str = str.slice(1);           // 移除已处理的字符
        }
        break;
        
      case State.tagName:
        // 解析标签名称状态:检查当前字符
        if (isAlpha(char)) {
          chars.push(char);             // 继续添加到 chars 数组
          str = str.slice(1);           // 移除已处理的字符
        } else if (char === ">") {
          currentState = State.initial; // 标签名称结束,返回初始状态
          tokens.push({
            type: "tag",
            name: chars.join("")
          });                          // 创建标签类型的 token
          chars.length = 0;             // 清空 chars 数组
          str = str.slice(1);           // 移除已处理的字符
        }
        break;
        
      case State.text:
        // 解析文本节点状态:检查当前字符
        if (isAlpha(char)) {
          chars.push(char);             // 继续添加到 chars 数组
          str = str.slice(1);           // 移除已处理的字符
        } else if (char === "<") {
          currentState = State.tagOpen; // 遇到新的标签,返回标签开启状态
          tokens.push({
            type: "text",
            content: chars.join("")
          });                          // 创建文本类型的 token
          chars.length = 0;             // 清空 chars 数组
          str = str.slice(1);           // 移除已处理的字符
        }
        break;
        
      case State.tagEnd:
        // 结束标签的开始状态:检查当前字符
        if (isAlpha(char)) {
          currentState = State.tagEndName; // 进入结束标签名称状态
          chars.push(char);                  // 将字符添加到 chars 数组
          str = str.slice(1);                // 移除已处理的字符
        }
        break;
        
      case State.tagEndName:
        // 解析结束标签名称状态:检查当前字符
        if (isAlpha(char)) {
          chars.push(char);                  // 继续添加到 chars 数组
          str = str.slice(1);                // 移除已处理的字符
        } else if (char === ">") {
          currentState = State.initial;      // 结束标签名称结束,返回初始状态
          tokens.push({
            type: "tagEnd",
            name: chars.join("")
          });                                // 创建结束标签类型的 token
          chars.length = 0;                   // 清空 chars 数组
          str = str.slice(1);                // 移除已处理的字符
        }
        break;
    }
  }
  
  return tokens;
}

/**
 * 判断字符是否为字母
 * @param {string} char - 字符
 * @returns {boolean}
 */
function isAlpha(char) {
  return /[a-zA-Z]/.test(char);
}

2.3 构造 AST

上一步已经完成了 token 的解析。接下来需要根据这些 token 来创建模板的 AST。

核心思路

扫描整个 token 列表,使用栈结构来维护元素间的父子关系:

示例输入:

'<div><p>Vue</p><p>React</p></div>'

输出(Tokens):

[
  { type: "tag", name: "div" },
  { type: "tag", name: "p" },
  { type: "text", content: "Vue" },
  { type: "tagEnd", name: "p" },
  { type: "tag", name: "p" },
  { type: "text", content: "React" },
  { type: "tagEnd", name: "p" },
  { type: "tagEnd", name: "div" }
]

AST 构造过程

使用 elementStack 栈来维护元素间的父子关系:

步骤Token操作栈状态(从底到顶)
1初始化创建 Root 节点[Root]
2div tag创建 Element 节点,压栈[Root, div]
3p tag创建 Element 节点,压栈[Root, div, p]
4Vue text创建 Text 节点,作为 p 的子节点[Root, div, p]
5p tagEnd弹出栈顶节点[Root, div]
6p tag创建 Element 节点,压栈[Root, div, p]
7React text创建 Text 节点,作为 p 的子节点[Root, div, p]
8p tagEnd弹出栈顶节点[Root, div]
9div tagEnd弹出栈顶节点[Root]

Parse 函数实现

/**
 * 将模板字符串解析为模板 AST
 * @param {string} str - 模板字符串
 * @returns {Object} - 模板 AST
 */
function parse(str) {
  // 1. 首先对模板进行 token 解析,得到对应的 tokens 数组
  const tokens = tokenize(str);
  
  // 2. 创建 Root 根 AST 节点
  const root = {
    type: 'Root',
    children: []
  };
  
  // 3. 创建 elementStack 栈,一开始只有 Root 根节点
  const elementStack = [root];
  
  // 4. 扫描 tokens 数组
  while (tokens.length > 0) {
    // 获取当前栈顶节点作为父节点
    const parent = elementStack[elementStack.length - 1];
    
    // 获取当前扫描的 token
    const t = tokens[0];
    
    // 根据 token 的不同类型,创建不同的 AST 节点
    switch (t.type) {
      case 'tag':
        // 创建 Element 类型的 AST 节点
        const elementNode = {
          type: 'Element',
          tag: t.name,
          children: []
        };
        // 将其添加到父级节点的 children 中
        parent.children.push(elementNode);
        // 将当前节点压入栈
        elementStack.push(elementNode);
        break;
        
      case 'text':
        // 创建 Text 类型的 AST 节点
        const textNode = {
          type: 'Text',
          content: t.content
        };
        // 将其添加到父级节点的 children 中
        parent.children.push(textNode);
        break;
        
      case 'tagEnd':
        // 遇到结束标签,将当前栈顶的节点弹出
        elementStack.pop();
        break;
    }
    
    // 消费已经扫描过的 token
    tokens.shift();
  }
  
  return root;
}

最终生成的 AST

{
  "type": "Root",
  "children": [
    {
      "type": "Element",
      "tag": "div",
      "children": [
        {
          "type": "Element",
          "tag": "p",
          "children": [
            {
              "type": "Text",
              "content": "Vue"
            }
          ]
        },
        {
          "type": "Element",
          "tag": "p",
          "children": [
            {
              "type": "Text",
              "content": "React"
            }
          ]
        }
      ]
    }
  ]
}

3. 转换器

转换器的核心作用就是:负责将模板 AST 转换为 JavaScript AST

整体来讲,转换器的编写分为两大部分:

  1. 模板 AST 的遍历与转换
  2. 生成 JS AST

3.1 模板 AST 的遍历与转换

步骤一:AST 节点信息打印工具

首先书写一个简单的工具方法,方便查看一个模板 AST 中的节点信息。

/**
 * 打印 AST 节点信息
 * @param {Object} node - AST 节点
 * @param {number} indent - 缩进级别
 */
function dump(node, indent = 0) {
  // 获取当前节点的类型
  const type = node.type;
  
  // 根据节点类型构建描述信息
  const desc = node.type === "Root"
    ? ""
    : node.type === "Element"
    ? node.tag
    : node.content;
  
  // 打印当前节点信息,使用"-"字符表示缩进层级
  console.log(`${"-".repeat(indent)}${type}: ${desc}`);
  
  // 如果当前节点有子节点,递归调用 dump 函数打印每个子节点
  if (node.children) {
    node.children.forEach((n) => dump(n, indent + 2));
  }
}

步骤二:基础遍历实现

遍历整棵模板 AST 树,在遍历的途中可以做修改。

/**
 * 遍历并转换 AST 节点(基础版本)
 * @param {Object} ast - 模板 AST
 */
function traverseNode(ast) {
  const currentNode = ast;
  
  // 检查当前节点是否为元素节点,并且标签名为"p"
  if (currentNode.type === "Element" && currentNode.tag === "p") {
    currentNode.tag = "h1"; // 将标签名从"p"改为"h1"
  }
  
  // 获取当前节点的子节点
  const children = currentNode.children;
  if (children) {
    // 如果当前节点有子节点,遍历它们
    for (let i = 0; i < children.length; i++) {
      // 递归调用 traverseNode 函数来处理每个子节点
      traverseNode(children[i]);
    }
  }
}

/**
 * 转换模板 AST
 * @param {Object} ast - 模板 AST
 */
function transform(ast) {
  traverseNode(ast);
  console.log(dump(ast));
}

步骤三:遍历与转换解耦

让遍历和转换进行解耦,通过 context 上下文对象来实现。

/**
 * 转换模板 AST(解耦版本)
 * @param {Object} ast - 模板 AST
 */
function transform(ast) {
  const context = {
    // 用于存储当前正在转换的节点
    currentNode: null,
    // 用于存储当前正在转换的子节点在父节点的 children 数组中的索引
    childIndex: 0,
    // 用于存储当前正在转换的父节点
    parent: null,
    // 用于存储具体的转换函数
    nodeTransforms: [transformElement, transformText],
  };
  
  // 调用 traverseNode 函数来遍历和转换 AST
  traverseNode(ast, context);
  // 打印转换后的 AST 结构
  console.log(dump(ast));
}

/**
 * 遍历 AST 节点(带上下文)
 * @param {Object} ast - 模板 AST
 * @param {Object} context - 转换上下文
 */
function traverseNode(ast, context) {
  context.currentNode = ast;
  
  // 拿到转换方法的数组
  const transforms = context.nodeTransforms;
  
  // 遍历数组中的每个转换方法
  for (let i = 0; i < transforms.length; i++) {
    transforms[i](context.currentNode, context);
  }
  
  // 获取当前节点的子节点
  const children = context.currentNode.children;
  if (children) {
    // 如果当前节点有子节点,遍历它们
    for (let i = 0; i < children.length; i++) {
      // 更新当前上下文中的 parent 父节点
      context.parent = context.currentNode;
      // 索引也需要更新
      context.childIndex = i;
      // 递归调用 traverseNode 函数来处理每个子节点
      traverseNode(children[i], context);
    }
  }
}

转换函数实现

/**
 * 转换元素节点
 * @param {Object} node - AST 节点
 */
function transformElement(node) {
  if (node.type === "Element" && node.tag === "p") {
    node.tag = "h1"; // 将标签名从"p"改为"h1"
  }
}

/**
 * 转换文本节点
 * @param {Object} node - AST 节点
 */
function transformText(node) {
  if (node.type === "Text") {
    node.content = node.content.toUpperCase(); // 将文本内容转换为大写
  }
}

步骤四:增强上下文对象

继续完善 context 上下文对象,添加替换节点和删除节点的方法。

function transform(ast) {
  const context = {
    // 用于存储当前正在转换的节点
    currentNode: null,
    // 用于存储当前正在转换的子节点在父节点的 children 数组中的索引
    childIndex: 0,
    // 用于存储当前正在转换的父节点
    parent: null,
    
    // 新增:替换节点的方法
    replaceNode(node) {
      // 找到当前节点在父节点的 children 中的位置,并将其替换为新节点
      context.parent.children[context.childIndex] = node;
      // 将 currentNode 也更新为新节点
      context.currentNode = node;
    },
    
    // 新增:删除节点方法
    removeNode() {
      if (context.parent) {
        // 根据当前节点的索引删除当前节点
        context.parent.children.splice(context.childIndex, 1);
        // 将 currentNode 置为 null
        context.currentNode = null;
      }
    },
    
    // 用于存储具体的转换函数
    nodeTransforms: [transformElement, transformText],
  };
  
  traverseNode(ast, context);
  console.log(dump(ast));
}

步骤五:处理节点生命周期(进入和退出阶段)

不仅仅是在进入节点的时候处理一次,在退出节点的时候,也需要处理一次。

/**
 * 遍历 AST 节点(支持进入和退出阶段)
 * @param {Object} ast - 模板 AST
 * @param {Object} context - 转换上下文
 */
function traverseNode(ast, context) {
  console.log("处理节点:", ast.type, ast.tag || ast.content);
  
  context.currentNode = ast;
  
  // 对节点的访问分为两个阶段:进入阶段和退出阶段
  // 当转换函数处于进入阶段时,它会先进入父节点,再进入子节点
  // 而当转换函数处于退出阶段时,则会先退出子节点,再退出父节点
  // 这样,只要我们在退出节点阶段对当前访问的节点进行处理
  // 就一定能够保证其子节点全部处理完毕
  
  // 1. 增加退出阶段的回调函数数组
  const exitFns = [];
  
  // 拿到转换方法的数组
  const transforms = context.nodeTransforms;
  
  // 遍历数组中的每个转换方法
  for (let i = 0; i < transforms.length; i++) {
    // 2. 转换函数可以返回另外一个函数,该函数作为退出阶段的回调函数
    const onExit = transforms[i](context.currentNode, context);
    if (onExit) {
      exitFns.push(onExit);
    }
    
    // 由于任何转换函数都可能移除当前节点,因此每个转换函数执行完毕后
    // 都应该检查当前节点是否已经被移除,如果被移除了,直接返回即可
    if (!context.currentNode) return;
  }
  
  // 获取当前节点的子节点
  const children = context.currentNode.children;
  if (children) {
    // 如果当前节点有子节点,遍历它们
    for (let i = 0; i < children.length; i++) {
      // 更新当前上下文中的 parent 父节点
      context.parent = context.currentNode;
      // 另外索引也需要更新
      context.childIndex = i;
      // 递归调用 traverseNode 函数来处理每个子节点
      traverseNode(children[i], context);
    }
  }
  
  // 3. 在节点处理的最后阶段执行缓存在 exitFns 中的回调函数
  let i = exitFns.length;
  while (i--) {
    exitFns[i]();
  }
}

修改转换函数支持退出阶段

/**
 * 转换文本节点(支持退出阶段)
 * @param {Object} node - AST 节点
 * @param {Object} context - 转换上下文
 * @returns {Function|null} - 退出阶段回调函数
 */
function transformText(node, context) {
  return () => {
    console.log("可以再次处理节点:", node.type, node.tag || node.content);
  };
}

3.2 生成 JS AST

JS AST 结构分析

假设有如下代码:

function render() {
  return null;
}

对应的 JS AST 结构如下:

属性类型说明
idIdentifier函数名称
paramsArray函数参数列表
bodyArray函数体(可能包含多条语句)

函数声明节点结构

const FunctionDeclNode = {
  type: 'FunctionDecl',           // 代表该节点是一个函数声明
  id: {
    type: 'Identifier',
    name: 'render'                // name 用来存储函数名称
  },
  params: [],                      // 函数参数
  body: [
    {
      type: 'ReturnStatement',
      return: null
    }
  ]
}

关键 AST 节点类型

1. 标识符(Identifier)
{
  type: 'Identifier',
  name: 'h'  // 变量或函数名
}
2. 字符串字面量(StringLiteral)
{
  type: 'StringLiteral',
  value: 'div'  // 字符串值
}
3. 数组表达式(ArrayExpression)
{
  type: 'ArrayExpression',
  elements: []  // 数组元素列表
}
4. 函数调用表达式(CallExpression)
{
  type: 'CallExpression',
  callee: {
    type: 'Identifier',
    name: 'h'  // 被调用的函数名
  },
  arguments: []  // 调用参数列表
}

完整的渲染函数 AST

从模板 <div><p>Vue</p><p>React</p></div> 生成的渲染函数:

function render() {
  return h('div', [
    h('p', 'Vue'),
    h('p', 'React')
  ])
}

对应的完整 JS AST:

{
  "type": "FunctionDecl",
  "id": {
    "type": "Identifier",
    "name": "render"
  },
  "params": [],
  "body": [
    {
      "type": "ReturnStatement",
      "return": {
        "type": "CallExpression",
        "callee": {
          "type": "Identifier",
          "name": "h"
        },
        "arguments": [
          {
            "type": "StringLiteral",
            "value": "div"
          },
          {
            "type": "ArrayExpression",
            "elements": [
              {
                "type": "CallExpression",
                "callee": {
                  "type": "Identifier",
                  "name": "h"
                },
                "arguments": [
                  {
                    "type": "StringLiteral",
                    "value": "p"
                  },
                  {
                    "type": "StringLiteral",
                    "value": "Vue"
                  }
                ]
              },
              {
                "type": "CallExpression",
                "callee": {
                  "type": "Identifier",
                  "name": "h"
                },
                "arguments": [
                  {
                    "type": "StringLiteral",
                    "value": "p"
                  },
                  {
                    "type": "StringLiteral",
                    "value": "React"
                  }
                ]
              }
            ]
          }
        ]
      }
    }
  ]
}

辅助函数设计

/**
 * 创建字符串字面量节点
 * @param {string} value - 字符串值
 */
function createStringLiteral(value) {
  return {
    type: "StringLiteral",
    value,
  };
}

/**
 * 创建标识符节点
 * @param {string} name - 标识符名称
 */
function createIdentifier(name) {
  return {
    type: "Identifier",
    name,
  };
}

/**
 * 创建数组表达式节点
 * @param {Array} elements - 数组元素
 */
function createArrayExpression(elements) {
  return {
    type: "ArrayExpression",
    elements,
  };
}

/**
 * 创建函数调用表达式节点
 * @param {string} callee - 被调用的函数名
 * @param {Array} args - 调用参数列表
 */
function createCallExpression(callee, args) {
  return {
    type: "CallExpression",
    callee: createIdentifier(callee),
    arguments: args,
  };
}

节点转换函数实现

transformText - 文本节点转换
/**
 * 转换文本节点
 * @param {Object} node - AST 节点
 * @param {Object} context - 转换上下文
 */
function transformText(node, context) {
  if (node.type !== "Text") return;
  
  // 在模板 AST 节点上添加 jsNode 属性
  // 存储转换后的 JS AST 节点
  node.jsNode = createStringLiteral(node.content);
}
transformElement - 元素节点转换
/**
 * 转换元素节点
 * @param {Object} node - AST 节点
 * @param {Object} context - 转换上下文
 * @returns {Function} - 退出阶段回调函数
 */
function transformElement(node, context) {
  return () => {
    if (node.type !== "Element") return;
    
    // 1. 创建 h 函数调用的 AST 节点
    // 第一个参数是标签名(字符串)
    const callExp = createCallExpression("h", [
      createStringLiteral(node.tag),
    ]);
    
    // 2. 处理 h 函数的第二个参数(子节点)
    if (node.children.length === 1) {
      // 如果只有一个子节点,直接将子节点的 jsNode 作为参数
      callExp.arguments.push(node.children[0].jsNode);
    } else {
      // 如果有多个子节点,将子节点的 jsNode 组成数组传入
      callExp.arguments.push(
        createArrayExpression(
          node.children.map((child) => child.jsNode)
        )
      );
    }
    
    // 将生成的 JS 节点挂载到模板 AST 节点上
    node.jsNode = callExp;
  };
}
transformRoot - 根节点转换
/**
 * 转换根节点
 * @param {Object} node - AST 节点
 * @param {Object} context - 转换上下文
 * @returns {Function} - 退出阶段回调函数
 */
function transformRoot(node, context) {
  return () => {
    if (node.type !== "Root") return;
    
    // 获取根节点的第一个子节点(模板根元素)的 JS 节点
    const vnodeJSAST = node.children[0].jsNode;
    
    // 创建 render 函数声明的 JS AST 节点
    node.jsNode = {
      type: "FunctionDecl",
      id: {
        type: "Identifier",
        name: "render",
      },
      params: [],
      body: [
        {
          type: "ReturnStatement",
          return: vnodeJSAST,
        },
      ],
    };
  };
}

完整的转换函数

function transform(ast) {
  const context = {
    currentNode: null,
    childIndex: 0,
    parent: null,
    
    replaceNode(node) {
      context.parent.children[context.childIndex] = node;
      context.currentNode = node;
    },
    
    removeNode() {
      if (context.parent) {
        context.parent.children.splice(context.childIndex, 1);
        context.currentNode = null;
      }
    },
    
    nodeTransforms: [transformRoot, transformElement, transformText],
  };
  
  traverseNode(ast, context);
}

4. 生成器

生成器的作用:根据 JS AST 生成最终的渲染函数代码

/**
 * Vue 模板编译器主函数
 * @param {string} template - 模板字符串
 * @returns {string} - 生成的渲染函数代码
 */
function compile(template) {
  // 1. 得到模板 AST
  const ast = parse(template);
  
  // 2. 将模板 AST 转换为 JavaScript AST
  transform(ast);
  
  // 3. 代码生成
  const code = generate(ast.jsNode);
  
  return code;
}

4.1 生成器上下文设计

和上一步转换器类似,在生成器中也需要维护一个上下文对象,该上下文对象用于维护代码生成过程中程序的运行状态。

/**
 * 代码生成函数
 * @param {Object} node - JS AST 节点
 * @returns {string} - 生成的代码字符串
 */
function generate(node) {
  // 上下文对象
  const context = {
    // 存储最终所生成的代码
    code: "",
    
    // 在生成代码的时候,通过调用 push 方法来进行拼接
    push(code) {
      context.code += code;
    },
    
    // 当前缩进的级别,初始值为 0,也就是没有缩进
    currentIndent: 0,
    
    // 该方法用来换行,会根据当前缩进的级别来添加相应的缩进
    newline() {
      context.code += "\n" + `  `.repeat(context.currentIndent);
    },
    
    // 用来缩进,会将缩进级别加一
    indent() {
      context.currentIndent++;
      context.newline();
    },
    
    // 用来取消缩进,会将缩进级别减一
    deIndent() {
      context.currentIndent--;
      context.newline();
    },
  };
  
  genNode(node, context);
  return context.code;
}

4.2 代码生成实现

genNode - 节点分发函数

根据节点类型调用不同的生成方法。

/**
 * 根据节点类型生成代码
 * @param {Object} node - JS AST 节点
 * @param {Object} context - 生成器上下文
 */
function genNode(node, context) {
  switch (node.type) {
    case 'FunctionDecl':
      genFunctionDecl(node, context);
      break;
    case 'ReturnStatement':
      genReturnStatement(node, context);
      break;
    case 'CallExpression':
      genCallExpression(node, context);
      break;
    case 'StringLiteral':
      genStringLiteral(node, context);
      break;
    case 'ArrayExpression':
      genArrayExpression(node, context);
      break;
    default:
      throw new Error(`未知的节点类型: ${node.type}`);
  }
}

生成函数声明

/**
 * 生成函数声明代码
 * @param {Object} node - FunctionDecl 节点
 * @param {Object} context - 生成器上下文
 */
function genFunctionDecl(node, context) {
  const { push, indent, deIndent } = context;
  
  // 向输出中添加 "function 函数名"
  push(`function ${node.id.name} `);
  
  // 添加左括号开始参数列表
  push(`(`);
  
  // 生成参数列表
  genNodeList(node.params, context);
  
  // 添加右括号结束参数列表
  push(`) `);
  
  // 添加左花括号开始函数体
  push(`{`);
  
  // 缩进,为函数体的代码生成做准备
  indent();
  
  // 遍历函数体中的每个节点,生成相应的代码
  node.body.forEach((n) => genNode(n, context));
  
  // 减少缩进
  deIndent();
  
  // 添加右花括号结束函数体
  push(`}`);
}

生成节点列表

/**
 * 生成节点列表(如函数参数、数组元素)
 * @param {Array} nodes - 节点数组
 * @param {Object} context - 生成器上下文
 */
function genNodeList(nodes, context) {
  const { push } = context;
  
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    // 生成当前节点的代码
    genNode(node, context);
    
    // 如果当前节点不是最后一个节点,添加逗号分隔
    if (i < nodes.length - 1) {
      push(", ");
    }
  }
}

生成 return 语句

/**
 * 生成 return 语句代码
 * @param {Object} node - ReturnStatement 节点
 * @param {Object} context - 生成器上下文
 */
function genReturnStatement(node, context) {
  const { push } = context;
  
  // 添加 "return "
  push(`return `);
  
  // 生成 return 语句后面的代码
  genNode(node.return, context);
}

生成函数调用表达式

/**
 * 生成函数调用表达式代码
 * @param {Object} node - CallExpression 节点
 * @param {Object} context - 生成器上下文
 */
function genCallExpression(node, context) {
  const { push } = context;
  const { callee, arguments: args } = node;
  
  // 添加 "函数名("
  push(`${callee.name}(`);
  
  // 生成参数列表
  genNodeList(args, context);
  
  // 添加 ")"
  push(`)`);
}

生成字符串字面量

/**
 * 生成字符串字面量代码
 * @param {Object} node - StringLiteral 节点
 * @param {Object} context - 生成器上下文
 */
function genStringLiteral(node, context) {
  const { push } = context;
  
  // 添加 "'字符串值'"
  push(`'${node.value}'`);
}

生成数组表达式

/**
 * 生成数组表达式代码
 * @param {Object} node - ArrayExpression 节点
 * @param {Object} context - 生成器上下文
 */
function genArrayExpression(node, context) {
  const { push } = context;
  
  // 添加 "["
  push("[");
  
  // 生成数组元素
  genNodeList(node.elements, context);
  
  // 添加 "]"
  push("]");
}

5. 完整编译流程整合

完整的 compile 函数

/**
 * Vue 模板编译器主函数
 * @param {string} template - 模板字符串
 * @returns {string} - 生成的渲染函数代码
 */
function compile(template) {
  // 1. 解析:将模板字符串解析为模板 AST
  const ast = parse(template);
  
  // 2. 转换:将模板 AST 转换为 JS AST
  transform(ast);
  
  // 3. 生成:将 JS AST 生成为渲染函数代码
  const code = generate(ast.jsNode);
  
  return code;
}

测试生成的结果

const code = generate(ast.jsNode);
console.log(code);

/* 输出:
function render () {
  return h('div', [h('p', 'Vue'), h('p', 'React')])
}
*/

总结

编译流程回顾

模板字符串
  ↓ 解析器
Tokens
  ↓ AST 构造
模板 AST
  ↓ 转换器
JS AST
  ↓ 生成器
渲染函数代码

核心技术点

阶段核心技术关键数据结构
解析有限状态机Token、Stack
转换AST 遍历、生命周期钩子Context、转换函数
生成字符串拼接Code Context

关键设计模式

  1. 有限状态机(FSM):用于 token 识别和解析
  2. 栈结构:维护元素间的父子关系
  3. 上下文对象:存储转换/生成过程中的状态
  4. 访问者模式:遍历 AST 并对节点执行操作
  5. 生命周期钩子:进入和退出节点的处理

至此,Vue 模板编译器的完整实现原理已经全部讲解完毕! 🎉🎉🎉


-EOF-