Vue 模板编译原理解析

Vue 模板编译原理解析

模板编译整体流程

首先我们看一下什么是编译?

所谓编译(Compile),指的是将语言 A 翻译成语言 B,语言 A 就被称之为源码(source code),语言 B 就被称之为目标代码(target code),这个事情谁来做?编译器来做。编译器你也不用想得那么神秘,就是一段程序而已。

完整的编译流程一般包含以下几个步骤:

  • 词法分析:就是负责将源码拆解成一系列的词法单元(token)
  • 语法分析:将上一步所得到的词法单元组成一颗抽象语法树
  • 语义分析:主要就是根据上一步所得到的抽象语法树,进行深度遍历,检查语法规则是否符合要求
  • 中间代码生成
  • 优化
  • 目标代码生成

image-20231113155353850

上面的步骤,如果从大的方面去分,那么可以分为 编译前端编译后端

  • 编译前端:所谓编译前端,它通常是和目标平台无关的,仅仅是负责分析源代码
  • 编译后端:通常与目标平台有关系

回到 Vue,在 Vue 的模板里面就涉及到了编译的操作:

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

编译后的结果是什么?编译后的结果就是渲染函数

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

注意这里,整个编译过程并非一蹴而就,而是经历了一个又一个的步骤,一点一点转换而来的。

整体来讲,整个编译过程如下图:

image-20231113095532166

在编译器内部,实际上又分为了三个东西:

  • 解析器:负责将模板解析为对应的模板抽象语法树
  • 转换器:负责将上一步所得到模板抽象语法树转为 JS 抽象语法树
  • 生成器:负责将上一步所得到的 JS 抽象语法树生成目标代码

通过上面的图,我们还会发现一个很重要的东西,那就是 AST 扮演了非常重要的角色。

AST

什么是 AST 呢?

所谓 AST,英语全称 Abstract Syntax Tree,翻译成中文,就是抽象语法树。

什么又叫抽象语法树?这里我们可以采用分词方式。分为 抽象语法

先来说树。树的话是数据结构里面的一种,用于表示层次关系的集合。树的基本思想将数据组织成分层结构,每个节点有一个父节点和零个或者多个子节点

例如:

    A
   / \
  B   C
 / \   \
D   E   F

上面就是一个树结构,上面的树是一个完全二叉树。

树的结构的这种特点就让它在搜索、排序、存储以及表示层次这些需求方面有广泛的应用,常见的应用场景文档对象模型(DOM)、路由算法、数据库索引等,在这里,我们的抽象语法树,很明显也是一种树结构。

接下来语法树就非常好理解。

var a = 42;
var b = 5;
function addA(d) {
  return a + d;
}
var c = addA(2) + b;

上面的代码,对于 JS 引擎而言,其实就是一段字符串:

"var a = 42;var b = 5;function addA(d){return a + d;}var c = addA(2) + b;";

JS 引擎首先第一步是遍历上面的字符串,将源码字符串拆解为一个一个的词法单元(词法分析),词法单元又被称之为 token,它是最小的词法单元,词法单元一般就是关键字、操作符、数字、运算符。

例如上面的代码,在进行了词法分析后,会得到如下的 token:

Keyword(var) Identifier(a) Punctuator(=) Numeric(42) Punctuator(;) Keyword(var) Identifier(b) Punctuator(=) Numeric(5) Punctuator(;) Keyword(function) Identifier(addA) Punctuator(() Identifier(d) Punctuator()) Punctuator({) Keyword(return) Identifier(a) Punctuator(+) Identifier(d) Punctuator(;) Punctuator(}) Keyword(var) Identifier(c) Punctuator(=) Identifier(addA) Punctuator(() Numeric(2) Punctuator()) Punctuator(+) Identifier(b) Punctuator(;)

接下来下一步,就是根据这些 token,形成一颗树结构(语法分析):

image-20231113131026349

可以在 https://www.jointjs.com/demos/abstract-syntax-tree 或者 https://astexplorer.net/ 看到代码的抽象语法树。

目前我们已经搞懂什么是语法树。

为什么叫做 抽象 语法树 ?

**抽象**在计算机科学里面,是一种非常重要的思想。这里的抽象和现实生活中的抽象的说法是有区别。现实生活中的抽象往往是指“模糊、含糊不清、难以理解”,例如“他说的话很抽象”。

计算机科学里面的抽象,指的是将关键部分从细节中分离出来,忽略不必要的细节,专注问题的关键方面

例如面向对象编程里面,类其实就是对对象的一种抽象,类描述了对象的关键信息(有哪些属性,有哪些方法),再举一个例子,比如接口,在定义接口的时候,只会规定这个接口里面有哪些方法,不需要关心内部具体的实现。

回到我们的抽象语法树,在形成树结构的时候,同样会忽略一些不重要的,非关键的信息(比如空格、换行符),只会将关键的部分(关键字、标志符、运算符)生成到树结构里面。

理解 AST 非常重要,在开发中但凡涉及到 转换 的场景,都是基于抽象语法树来运作的

  • Typescript
image-20231113132827332
  • Babel
  • Prettier
  • ESLint

解析器

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

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

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

'<template><div><h1 :id="someId">Hello</h1></div></template>';

接下来我们的工作重点,就是解析这段字符串。

这里涉及到了 有限状态机 的概念。

FSM

英语全称 Finite State Machine,翻译成中文就是有限状态机,它首先会定义一组状态,然后会定义状态之间进行转移的事件。

来看一个具体的例子:

"<p>Vue</p>";

那么整个状态的迁移过程如下:

  1. 状态机一开始处于 初始状态
  2. 在初始状态下,读取字符串的第一个字符 < ,然后状态机的状态就会更新为 标签开始状态
  3. 读取下一个字符 p,由于 p 是字母,那么状态机的状态就会更新为 标签名称开始状态
  4. 读取下一个字符 >,状态机会回归为 初始状态
  5. 读取下一个字符 V,状态机的状态为 文本状态
  6. 下一个字符 u,状态机的状态为 文本状态
  7. 下一个字符 e,状态机的状态为 文本状态
  8. 读取下一个字符 < ,此时状态机会进入到 标签开始状态
  9. 读取下一个字符 / ,状态机的状态会变为 标签结束状态
  10. 读取下一个字符 p,状态机的状态为 标签名称结束状态
  11. 最后是 > ,状态机重新回到 初始状态

具体如下图所示:

image-20231113140436969

实际上,我们最熟悉的 HTML,浏览器引擎在内部进行解析的时候,也是通过有限状态机进行解析的。

接下来我们落地到代码,大致就如下:

const tempalte = "<p>Vue</p>";
// 首先需要定义一些状态
const state = {
  initial: 1, // 初始状态
  tagOpen: 2, // 标签开始状态
  tagName: 3, // 标签名称开始状态
  text: 4, // 文本状态
  tagEnd: 5, // 标签结束状态
  tagEndName: 6, // 标签名称结束状态
};

function tokenize(str) {
  let currentState = state.initial; // 一开始是初始状态
  const chars = []; // 用于缓存字符
  const tokens = []; // 用于存储最终分析出来的 tokens,并且作为函数的返回值

  while (str) {
    const char = str[0]; // 先取出第一个字符
    switch (currentState) {
      case state.initial: {
        // ...
        str = str.slice(1); // 消费一个字符
      }
      case state.tagOpen: {
        // ...
      }
    }
  }

  return tokens;
}

构造 AST

到目前为止,我们只是将模板解析为了一个一个的 token,任务只完成了一半,接下来需要根据上一步所得到的 tokens 来构造 AST 树。

构造 AST 的过程其实就是就是对 tokens 列表进行扫描的过程,从列表的第一个 token,按照顺序进行扫描,直到列表中所有 token 都被处理完毕。

在这个过程中,我们需要维护一个栈(这个也是一种数据结构),这个栈的作用主要使用用于维护元素间的父子关系,每遇到一个开始标签的节点,就会构造一个 Element 类型的 AST 节点,压入到栈里面。

来看一个具体的例子:

"<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" },
];

接下来我们就需要扫描这个 tokens

  1. 一开始会有一个栈,这个栈里面只有 Root 节点 [ Root ]
  2. 首先是 div tag,创建一个 Element 类型的 AST 节点,压栈,当前的栈为 [ Root, div ],div 就作为 Root 的子节点
image-20231113150248725
  1. 接下来是 p tag,创建一个 Element 类型的 AST 节点,压栈,当前的栈为 [ Root, div, p ],p 作为 div 的子节点。
image-20231113150335866
  1. 接下来是 Vue text 文本节点,此时就会创建一个 Text 类型的 AST 节点,作为 p 的子节点
image-20231113150356416
  1. 接下来是 p tagEnd,发现这是一个结束标签,此时就会将 p 出栈,当前的栈为 [ Root, div ]
  2. 接下来是 p tag,创建一个 Element 类型的 AST 节点,压栈,当前的栈为 [ Root, div, p ],p 作为 div 的子节点。
image-20231113150442450
  1. 接下来是 React text 文本节点,此时就会创建一个 Text 类型的 AST 节点,作为 p 的子节点
image-20231113150537351
  1. 接下来是 p tagEnd,发现这是一个结束标签,此时就会将 p 出栈,当前的栈为 [ Root, div ]
  2. 接下来是 div tagEnd,发现这是一个结束标签,此时就会将 div 出栈,当前的栈为 [ Root ]

最后落地到代码:

function parse(str) {
  // 首先对模板进行 token 解析,得到对应的 tokens 数组
  const tokens = tokenize(str);
  // 创建 Root 根 AST 节点的
  const root = {
    type: "Root",
    children: [],
  };
  // 创建 elementStack 栈,一开始只有 Root 根节点
  const elementStack = [root];
  // 直到 tokens 数组被全部扫描完才会推出
  while (tokens.length) {
    // 获取当前栈顶点作为父节点
    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":
        // 创建文本类型的 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": "Template"
              }
          ]
        }
      ]
    }
  ]
}

到目前为止,我们整个解析器的任务就完成了。

转换器

主要的目的是将模板的 AST 转换为 JS 的 AST,整个模板的编译过程如下:

// Vue 的模板编译器
function compile(template) {
  // 1. 得到模板的 AST
  const ast = parse(template);
  // 2. 将模板 AST 转为 JS AST
  transform(ast);
}

整个转换实际上可以分为两个大的部分:

  • 模板 AST 的遍历以及针对节点的操作能力
  • 生成 JavaScript AST

模板 AST 的遍历以及针对节点的操作能力

步骤一

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

// 打印 AST 节点
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}`);

  // 如果有子节点,递归打印
  if (node.children) {
    node.children.forEach((child) => dump(child, indent + 2));
  }
}

步骤二

接下来我们就需要遍历整棵模板的 AST 树,在遍历的时候就可以针对一些节点动一些手脚,例如我们要将所有的 p 修改为 h1

// 该方法就是用于遍历 AST 节点的
function traverseNode(ast) {
  // 获取当前节点
  const currentNode = ast;

  // 接下来我们就可以针对拿到的节点做一些事情
  if (currentNode.type === "Element" && currentNode.tag === "p") {
    // 如果是 p 标签,就将其转换为 h1 标签
    currentNode.tag = "h1";
  }

  // 如果有子节点,递归遍历
  const children = currentNode.children;
  if (children) {
    // 如果有子节点,那么我们就遍历
    for (let i = 0; i < children.length; i++) {
      traverseNode(children[i]);
    }
  }
}

// 负责将模板 AST 转换为 JavaScript AST
function transform(ast) {
  traverseNode(ast);
  console.log(dump(ast));
}

在上面的代码中,transform 是最终负责转换的方法,转换的核心逻辑是放在 transform 里面的。transform 里面决定了我整个转换操作,第一步做什么,第二步做什么。

traverseNode 负责遍历整个模板的 AST,并且在遍历的途中,我们还能够进行一些修改。

步骤三

目前为止,这个 traverseNode 方法既负责了遍历 AST 节点,又负责了转换的工作,假设我们有一个新的需求,例如要将文本全部转为大写,那么我们就必须要去修改 traverseNode

// 该方法就是用于遍历 AST 节点的
function traverseNode(ast) {
  // 获取当前节点
  const currentNode = ast;

  // 接下来我们就可以针对拿到的节点做一些事情
  if (currentNode.type === "Element" && currentNode.tag === "p") {
    // 如果是 p 标签,就将其转换为 h1 标签
    currentNode.tag = "h1";
  }

  if (currentNode.type === "Text") {
    // 如果是文本节点,就将其内容转换为大写
    currentNode.content = currentNode.content.toUpperCase();
  }

  // 如果有子节点,递归遍历
  const children = currentNode.children;
  if (children) {
    // 如果有子节点,那么我们就遍历
    for (let i = 0; i < children.length; i++) {
      traverseNode(children[i]);
    }
  }
}

这个时候,我们就需要让 遍历转换 进行一个解耦。

可以在 transform 里面维护一个上下文对象。

什么是上下文 ?

**上下文是一个非常非常非常重要且常见的概念,所谓上下文,指的是一个环境信息。**我们在执行代码的时候,我们是需要一些数据的,那你的这些个数据从哪里去获取?就是从上下文环境中去获取。

实际上在现实生活中也有类似的上下文环境的场景,比如你在厨房做饭,整个厨房就是你做饭的环境,厨房里面有你要做饭的时候用到的各种厨具,比如菜刀、案板、锅、碗,灶台,这些工具整体构成了一个环境(上下文环境),当你做饭的时候要用到某一样工具,直接从这个环境中去获取。

16697040882418

上下文在很多地方都很常见:

  • React 中可以使用 React.createContext 创建一个上下文,其他组件可以访问该上下文里面的数据。
  • Vue 里面也有类似的概念,provide/inject 被称之为依赖注入,本质上也是提供了一个上下文环境。
  • Koa 里面的中间件接收一个 context 参数,本质上也是一个上下文对象
  • 还有就是我们最最最熟悉的 JS 里面的执行上下文。

接下来修改 transform,在内部维护一个 context 上下文对象:

const context = {
  currentNode: null, // 用于存储当前正在转换的节点
  childIndex: 0, // 存储当前正在转换的子节点在父节点的 children 数组中的索引
  parent: null, // 存储当前正在转换的节点的父节点
  nodeTransforms: [transformElement, transformText], // 这里面会放置各种转换函数
};

步骤四

接下来我们可以继续完善 context 这个上下文对象,可以添加一些方法,例如替换节点的方法以及删除节点的方法,如下:

const context = {
  currentNode: null, // 用于存储当前正在转换的节点
  childIndex: 0, // 存储当前正在转换的子节点在父节点的 children 数组中的索引
  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: [transformElement, transformText], // 这里面会放置各种转换函数
};

步骤五

最后我们还需要解决一个问题,那就是节点处理的次数问题。

目前我们使用的是深度优先遍历的方式来处理的节点。这种工作流方式有一个问题,在转换 AST 节点的过程中,往往需要根据子节点的情况来决定当前节点如何进行转换,这就要求父节点的转换操作必须等到子节点完毕后在执行。

这里我们可以对转换函数进行一个改造,让它返回一个方法,这个方法就是之后要再次处理的回调方法。

function transformText(node, context) {
  // ...

  // 返回一个回掉方法,这个回掉方法是在退出阶段执行的
  return () => {
    console.log("可以再次处理节点:", node.type, node.tag || node.content);
  };
}

之后最核心的是要对 traverseNode 方法进行一个改造:

// 该方法就是用于遍历 AST 节点的
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++) {
    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 以及 childIndex
      context.parent = context.currentNode;
      context.childIndex = i;
      traverseNode(children[i], context);
    }
  }

  // 3. 在节点处理的最后节点,执行缓存在 exitFns 数组中的所有回调函数
  let i = exitFns.length;
  while (i--) {
    exitFns[i]();
  }
}

通过这种方式,我们就可以在节点进入和退出的时候做处理。这个思想在很多地方也很常见:

  • React 中 beginWork 和 completeWork
  • Koa 中间件采用的洋葱模型

生成 JavaScript AST

我们要对整个模板的 AST 进行转换,转换为 JS AST。

我们目前的代码已经有了遍历模板 AST,并且针对不同的节点,做不同操作的能力。

我们首先需要知道 JS AST 长什么样子:

function render() {
  return null;
}

上面的代码,所对应的 JS AST 如下图所示:

image-20231120143716229

这里有几个比较关键的部分:

  • id:对应的是我函数的名称,类型为 Identifier
  • params:对应的是函数的参数,是一个数组的形式来表示的
  • body:对应的是函数体,由于函数体是可以有多条语句的,因此也是一个数组

我们仿造上面的设计,自己设计一个基本的数据结构来描述函数声明语句:

const FunctionDeclNode = {
  type: "FunctionDecl", // 表示该节点是一个函数声明
  id: {
    type: "Identifier",
    name: "render", // 函数的名称
  },
  params: [],
  body: [
    {
      type: "ReturnStatement",
      return: null,
    },
  ],
};

回到我们上面的模板:

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

转换出来的渲染函数:

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

根据渲染函数所对应的 AST 去分析对应的节点

下面说一下几个比较重要的节点:

h 函数对应的节点:

const callExp = {
  type: "CallExpression",
  callee: {
    type: "Identifier",
    name: "h",
  },
};

字符串所对应的节点:

const Str = {
  type: "StringLiteral",
  value: "div",
};

数组对应的节点:

const Arr = {
  type: "ArrayExpression",
  elements: [],
};

分析完节点之后,那么我们上面的那个 render 函数所对应的 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"}
                            ]
                        }
                    ]
                  }
              ]
          }
      }
  ]
}

分析完结构之后,下一步我们就是书写对应的转换函数。在转换函数之前,我们需要一些辅助函数,这些辅助函数用于帮助我们创建 JS AST 的节点:

function createStringLiteral(value) {
  return {
    type: "StringLiteral",
    value,
  };
}

function createIdentifier(name) {
  return {
    type: "Identifier",
    name,
  };
}

function createArrayExpression(elements) {
  return {
    type: "ArrayExpression",
    elements,
  };
}

function createCallExpression(callee, args) {
  return {
    type: "CallExpression",
    callee: createIdentifier(callee),
    arguments: args,
  };
}

接下来,我们就需要去修改我们的转换函数,一个有三个转换函数,分别是:

  • transformText
function transformText(node, context) {
  if (node.type !== "Text") return;
  node.jsNode = createStringLiteral(node.content);
}
  • transformElement
// 接下来我们就可以书写一些转换函数
// 将之前写在 traverseNode 里面的各种转换逻辑抽离出来了
function transformElement(node) {
  // 对外部返回一个函数,这个函数就是在退出节点时要执行的回调函数
  return () => {
    if (node.type !== "Element") return;
    // 1. 创建 h 函数的 AST 节点
    const callExp = createCallExpression("h", [createStringLiteral(node.tag)]);
    // 2. 处理 h 函数里面的参数
    node.children.length === 1
      ? // 如果之后一个子节点,那么直接将子节点的 jsNode 作为参数即可
        callExp.arguments.push(node.children[0].jsNode)
      : // 如果是多个子节点,那么就需要将子节点的 jsNode 作为数组传入
        callExp.arguments.push(
          createArrayExpression(node.children.map((child) => child.jsNode))
        );
  };
}
  • transformRoot
// 最后再写一个转换函数,负责转换 Root 根节点
function transformRoot(node) {
  return () => {
    if (node.type !== "Root") return;
    // 生成最外层的节点
    const vnodeJSAST = node.children[0].jsNode;
    node.jsNode = {
      type: "FunctionDecl",
      id: {
        type: "Identifier",
        name: "render",
      },
      params: [],
      body: [
        {
          type: "ReturnStatement",
          return: vnodeJSAST,
        },
      ],
    };
  };
}

最后在 transform 中使用这三个转换函数

生成器

整理一下思绪,哪怕你前面都没有听懂,但是你需要知道我们走到哪一步了。

目前我们已经有 js ast,只剩下最后一步,革命就成功了。

遍历这个生成的 js ast,转为具体的渲染函数

function compile(template) {
  // 1. 得到模板的 AST
  const ast = parse(template);
  // 2. 将模板 AST 转为 JS AST
  transform(ast);
  // 3. 代码生成
  const code = genrate(ast.jsNode);

  return code;
}

和转换器一样,我们在生成器内部也需要维护一个上下文对象,为我们提供一些辅助函数和必要的信息:

// 和上一步转换器非常相似,我们也需要一个上下文对象
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 方法,而 genNode 方法的内部,就是根据不同的 AST 节点类型,调用对应的生成方法:

function genNode(node, context) {
  // 我这里要做的事情,就是根据你当前节点的 type 来调用不同的方法
  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;
  }
}

每一种生成方法本质都非常简单,就是做字符串的拼接:

// 之后我们要做的就是完善上面的各种生成方法,而每一种生成方法的实质其实就是做字符串的拼接

// 生成函数声明
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(`}`);
}

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 语句
function genReturnStatement(node, context) {
  const { push } = context;
  // 添加 "return "
  push(`return `);
  // 生成 return 语句后面的代码
  genNode(node.return, context);
}

// 生成函数调用表达式
function genCallExpression(node, context) {
  const { push } = context;
  const { callee, arguments: args } = node;

  // 添加 "函数名("
  push(`${callee.name}(`);
  // 生成参数列表
  genNodeList(args, context);
  // 添加 ")"
  push(`)`);
}

// 生成字符串字面量
function genStringLiteral(node, context) {
  const { push } = context;
  // 添加 "'字符串值'"
  push(`'${node.value}'`);
}

// 生成数组表达式
function genArrayExpression(node, context) {
  const { push } = context;
  // 添加 "["
  push("[");
  // 生成数组元素
  genNodeList(node.elements, context);
  // 添加 "]"
  push("]");
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/289746.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

清风数学建模笔记-主成分分析

内容&#xff1a;主成分分析 介绍&#xff1a; 主成分分析是一种降维算法&#xff0c;它通过旋转和变换将多个指标转化为少数几个主成分&#xff0c;这些主成分是原变量的线性组合&#xff0c;且互不相关&#xff0c;其能反映出原始数据的大部分信息。 例如解决多重共线性问题…

Vue+ElementUI笔记(1)

一、表格 1.上移、下移和移除功能 需求&#xff1a;有时我们会面对类似这样的表格 图中的上移&#xff0c;下移功能需求明显要求我们改变两行数据的顺序。在实际开发中这种功能一般由后台来做&#xff0c;因为列表数据一般从后台获取刷新。即是我们点击”上移“&#xff0c;向…

K8Spod组件

一个pod能包含几个容器 一个pause容器(基础容器/父容器/根容器&#xff09; 一个或者多个应用容器(业务容器) 通常一个Pod最好只包含一个应用容器&#xff0c;一个应用容器最好也只运行一个业务进程。 同一个Pod里的容器都是运行在同一个node节点上的&#xff0c;并且共享 net、…

20、Finetuning

微调是指调整大型语言模型&#xff08;LLM&#xff09;的参数以适应特定任务的过程&#xff0c;用于改进预训练模型的性能。这是通过在与任务相关的数据集上训练模型来完成的。所需的微调量取决于任务的复杂性和数据集的大小。 PEFT&#xff08;Parameter-Efficient Fine-Tunin…

前端发开的性能优化 请求级:请求前(资源预加载和预读取)

预加载 预加载&#xff1a;是优化网页性能的重要技术&#xff0c;其目的就是在页面加载过程中先提前请求和获取相关的资源信息&#xff0c;减少用户的等待时间&#xff0c;提高用户的体验性。预加载的操作可以尝试去解决一些类似于减少首次内容渲染的时间&#xff0c;提升关键资…

逻辑回归(LR)----机器学习

基本原理 逻辑回归&#xff08;Logistic Regression&#xff0c;LR&#xff09;也称为"对数几率回归"&#xff0c;又称为"逻辑斯谛"回归。 logistic回归又称logistic 回归分析 &#xff0c;是一种广义的线性回归分析模型&#xff0c;常用于数据挖掘&#…

基于Rangenet Lib的自动驾驶LiDAR点云语义分割与可视化

这段代码是一个C程序&#xff0c;用于处理来自KITTI数据集的激光雷达&#xff08;LiDAR&#xff09;扫描数据。程序主要实现以下功能&#xff1a; 1. **读取和解析命令行参数**&#xff1a;使用Boost库中的program_options模块来定义和解析命令行参数。这包括扫描文件路径、模型…

李沐机器学习系列2--- mlp

1 Introduction LP中有一个很强的假设&#xff0c;输入和输出是线性关系&#xff0c;这一般是不符合事实的。 通过几何的方式去对信息进行理解和压缩是比较高效的&#xff0c;MLP可以表示成下面的形式。 1.1 从线性到非线性 X ∈ R n d X \in R^{n \times d} X∈Rnd表示输入…

深信服技术认证“SCCA-C”划重点:云计算关键技术

为帮助大家更加系统化地学习云计算知识&#xff0c;高效通过云计算工程师认证&#xff0c;深信服特推出“SCCA-C认证备考秘笈”&#xff0c;共十期内容。“考试重点”内容框架&#xff0c;帮助大家快速get重点知识。 划重点来啦 *点击图片放大展示 深信服云计算认证&#xff08…

神经网络:经典模型热门模型

在这里插入代码片【一】目标检测中IOU的相关概念与计算 IoU&#xff08;Intersection over Union&#xff09;即交并比&#xff0c;是目标检测任务中一个重要的模块&#xff0c;其是GT bbox与pred bbox交集的面积 / 二者并集的面积。 下面我们用坐标&#xff08;top&#xff0…

电动汽车BMS PCB制板的技术分析与可制造性设计

随着电动汽车行业的迅猛发展&#xff0c;各大厂商纷纷投入巨资进行技术研发和创新。电动汽车的核心之一在于其电池管理系统&#xff08;Battery Management System, BMS&#xff09;&#xff0c;而BMS的心脏则是其印刷电路板&#xff08;PCB&#xff09;。通过这篇文章探讨电动…

Application layer

title: 应用层 date: 2023-12-20 21:03:48 tags: 知识总结 categories: 计算机网络 应用层&#xff1a;负责最直观的应用请求的封装、发起 一、域名系统DNS 连接在互联网上的主机不仅有IP地址&#xff0c;还有便于用户记忆的主机名字。域名系统DNS能够把互联网上的主机的名字…

Idea启动运行“错误:java: 无效的源发行版: 13”,如何解决?

以上是以JDK1.8的项目作为举例&#xff0c;如果您用的是其他版本请选择对应的language level idea中项目的language level的含义 language level指的是编译项目代码所用的jdk版本。那么&#xff0c;从这个定义出发会有两个小问题。 ❶ 如果project sdk是jdk8&#xff0c;那么la…

卡尔曼滤波算法

卡尔曼滤波算法是一种经典的状态估计算法&#xff0c;它广泛应用于控制领域和信号处理领域。在电动汽车领域中&#xff0c;卡尔曼滤波算法也被广泛应用于电池管理系统中的电池状态估计。其中&#xff0c;电池的状态包括电池的剩余容量&#xff08;SOC&#xff09;、内阻、温度等…

openGauss学习笔记-185 openGauss 数据库运维-升级-提交升级/升级版本回退/异常处理

文章目录 openGauss学习笔记-185 openGauss 数据库运维-升级-提交升级/升级版本回退/异常处理185.1 提交升级操作步骤 185.2 升级版本回滚操作步骤 185.3 异常处理升级问题FAQ openGauss学习笔记-185 openGauss 数据库运维-升级-提交升级/升级版本回退/异常处理 185.1 提交升级…

Swift并发的结构化编程

并发&#xff08;concurrency&#xff09; 早期的计算机 CPU 都是单核的&#xff0c;操作系统为了达到同时完成多个任务的效果&#xff0c;会将 CPU 的执行时间分片&#xff0c;多个任务在同一个 CPU 核上按时间先后交替执行。由于 CPU 执行速度足够地快&#xff0c;给人的错觉…

基于Java+SpringBoot+vue+elementUI私人健身教练预约管理系统设计实现

基于JavaSpringBootvueelementUI私人健身教练预约管理系统设计实现 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式 文章目录 基于JavaSpringBootvueelementUI私人健身教练预约管理系统设计实现一、前言介绍&#xff1a;二、系统设计&#xff1a;2.1 性能需求分析2.2 B/S架构&…

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK实现相机掉线自动重连(C#)

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK实现相机掉线自动重连&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机的掉线自动重连的技术背景通过PnP事件函数检查Baumer工业相机是否掉线在NEOAPI SDK里实现相机掉线重连方法&#xff1a;工业相机掉线重连测试演示图…

Python武器库开发-武器库篇之代理池配置(四十)

武器库篇之代理池配置(四十) 我们在渗透的过程中&#xff0c;是必须要挂代理的&#xff0c;相信为何要挂代理的原因&#xff0c;各位也是非常的明白的&#xff0c;这里就不多讲了。关于如何挂代理和购买代理大家可以去看内网隧道代理技术&#xff08;十&#xff09;之公网资产…

优雅地展示20w单细胞热图|非Doheatmap 超大数据集 细胞数太多

单细胞超大数据集的热图怎么画&#xff1f;昨天刚做完展示20万单细胞的热图要这么画吗&#xff1f; 今天就有人发消息问我为啥他画出来的热图有问题。 问题起源 昨天分享完 20万单细胞的热图要这么画吗&#xff1f;&#xff0c;就有人问为啥他的数据会出错。我们先来看下他的…