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];// 当前扫描的 tokenconst 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;}// 消费已经扫描过的 tokentokens.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. 得到模板的 ASTconst ast = parse(template);// 2. 将模板 AST 转为 JS ASTtransform(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 以及 childIndexcontext.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. 得到模板的 ASTconst ast = parse(template);// 2. 将模板 AST 转为 JS ASTtransform(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("]");
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【QML】与 C++ 混合编程:互相调用函数

文章目录 qml 调用 C 函数案例 a&#xff1a;Q_INVOKABLE 标记 C 函数 视图设置进 qml 属性案例 b&#xff1a;qml 通过发送信号的方式&#xff0c;调用 Qt 槽函数 C调用qml函数 qml 调用 C 函数 qml 要使用 C 的函数有两个方法&#xff1a; 一种是&#xff0c;用 Q_INVOKABLE…

STM32 学习(三)OLED 调试工具

目录 一、简介 二、使用方法 2.1 接线图 2.2 配置引脚 2.3 编写代码 三、Keil 工具调试 一、简介 在进行单片机开发时&#xff0c;有很多调试方法&#xff0c;如下图&#xff1a; 其中 OLED 就是一种比较好用的调试工具&#xff1a; OLED 硬件电路如下&#xff0c…

7步教你如何快速建立电子商务网站

如果您需要以客户为中心搭建电子商务网站&#xff0c;或者您正在寻求开展电子商务业务&#xff0c;那么您很幸运——现在是开始的最佳时机&#xff01;事实上&#xff0c;在疫情期间&#xff0c;电子商务销售额增长了 50%。电子商务现在是一个价值 8700 亿美元的产业。随着如此…

深入了解Apache 日志,Apache 日志分析工具

Apache Web 服务器在企业中广泛用于托管其网站和 Web 应用程序&#xff0c;Apache 服务器生成的原始日志提供有关 Apache 服务器托管的网站如何处理用户请求以及访问您的网站时经常遇到的错误的重要信息。 什么是 Apache 日志 Apache 日志包含 Apache Web 服务器处理的所有事…

rime中州韵小狼毫 inputShow lua Filter 输入字符透传滤镜

在 rime中州韵小狼毫 inputShow lua Translator 一文中&#xff0c;我们通过 inputShow.lua 定制了 inputShow_translator&#xff0c;这使得我们的输入方案可以将用户输入的字符透传到候选列表中来。如下&#x1f447;&#xff1a; &#x1f446;上图中我们在候选列表中看到了…

音视频通信

文章目录 一、音视频通信流程二、流媒体协议1、RTSP2、RTMP3、HLS4、WebRTC 一、音视频通信流程 音视频通信完整流程有如下几个环节&#xff1a;采集、编码、前后处理、传输、解码、缓冲、渲染等。 每一个细分环节&#xff0c;还有更细分的技术模块。比如&#xff0c;前后处…

[足式机器人]Part2 Dr. CAN学习笔记-Ch01自动控制原理

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记-Ch01自动控制原理 1. 开环系统与闭环系统Open/Closed Loop System1.1 EG1: 烧水与控温水壶1.2 EG2: 蓄水与最终水位1.3 闭环控制系统 2. 稳定性分析Stability2.1 序言2.2 稳定的分类2.3 稳定的对…

[蓝桥杯2020国赛]答疑

答疑 题目描述 有 n 位同学同时找老师答疑。每位同学都预先估计了自己答疑的时间。 老师可以安排答疑的顺序&#xff0c;同学们要依次进入老师办公室答疑。 一位同学答疑的过程如下&#xff1a; 首先进入办公室&#xff0c;编号为 i 的同学需要 si​ 毫秒的时间。然后同学问…

2020年认证杯SPSSPRO杯数学建模D题(第一阶段)让电脑桌面飞起来全过程文档及程序

2020年认证杯SPSSPRO杯数学建模 D题 让电脑桌面飞起来 原题再现&#xff1a; 对于一些必须每天使用电脑工作的白领来说&#xff0c;电脑桌面有着非常特殊的意义&#xff0c;通常一些频繁使用或者比较重要的图标会一直保留在桌面上&#xff0c;但是随着时间的推移&#xff0c;…

Java数据结构:1. 数据结构前置知识

文章目录 一、初识数据结构二、初识集合框架1. 什么是集合框架2. 集合框架的重要性3. 背后所涉及的数据结构以及算法 三、时间复杂度空间复杂度1. 算法效率2. 时间复杂度&#xff08;1&#xff09;概念&#xff08;2&#xff09;大O的渐进表示法&#xff08;3&#xff09;推导大…

捕捉“五彩斑斓的黑”:锗基短波红外相机的多种成像应用

红外处于人眼可观察范围以外&#xff0c;为我们了解未知领域提供了新的途径。红外又可以根据波段范围&#xff0c;分为短波红外、中波红外与长波红外。较短的SWIR波长——大约900nm-1700nm——与可见光范围内的光子表现相似。虽然在SWIR中目标的光谱含量不同&#xff0c;但所产…

PostgreSQL荣获DB-Engines 2023年度数据库

数据库流行度排名网站 DB-Engines 2024 年 1 月 2 日发布文章宣称&#xff0c;PostgreSQL 荣获 2023 年度数据库管理系统称号。 PostgreSQL 在过去一年中获得了比其他 417 个产品更多的流行度增长&#xff0c;因此获得了 2023 年度 DBMS。 DB-Engines 通过计算每种数据库 2024 …

Amazon CodeWhisperer 免费 AI 代码生成助手体验分享

今年上半年&#xff0c;亚马逊云科技正式推出了实时AI编程助手 Amazon CodeWhisperer&#xff0c;还提供了供所有开发人员免费使用的个人版版本。经过一段时间的体验&#xff0c;我觉得 CodeWhisperer 可以处理编程工作中遇到的很多问题&#xff0c;并且帮助开发人员提高编程效…

高德地图经纬度坐标导出工具

https://tool.xuexiareas.com/map/amap 可以导出单个点&#xff0c;也可以导出多个&#xff0c;多个点可以连成线&#xff0c;可用于前端开发时自己模拟“线“数据

【Apache Doris】自定义函数之 JAVA UDF 详解

【Apache Doris】自定义函数之 JAVA UDF 详解 一、背景说明二、原理简介三、环境信息3.1 硬件信息3.2 软件信息 四、IDE准备五、JAVA UDF开发流程5.1 源码准备5.1.1 pom.xml5.1.2 JAVA代码 5.2 mvn打包5.2.1 clean5.2.2 package 5.3 函数使用5.3.1 upload5.3.2 使用 六、注意事…

华为端口隔离高级用法经典案例

最终效果&#xff1a; pc4不能ping通pc5&#xff0c;pc5能ping通pc4 pc1不能和pc2、pc3通&#xff0c;但pc2和pc3能互通 vlan batch 2 interface Vlanif1 ip address 10.0.0.254 255.255.255.0 interface Vlanif2 ip address 192.168.2.1 255.255.255.0 interface MEth0/0/1 i…

服务器防护怎么做

随着网络攻击的日益猖獗&#xff0c;服务器安全已成为关注的焦点。如何有效防御各种网络威胁&#xff0c;确保数据安全与业务连续性&#xff0c;已成为一项迫切的需求。目前服务器所面临的主要威胁包括但不限于&#xff1a;DDoS攻击、SQL注入、跨站脚本攻击(XSS)、远程命令执行…

大模型笔记 【1】 大模型初探

以下是Andrej Karpathy一小时讲解chatgpt的笔记。 Andrej Karpathy做自动驾驶的人应该比较熟悉&#xff0c;他是李飞飞的学生。在openAI做了一年半的科学家之后&#xff0c;去了特斯拉。在Tesla AI day讲解tesla自动驾驶方案的就是他。 这里我的主要收获是两个 大模型是一个有…

七夕祭

title: 七夕祭 date: 2024-01-03 22:47:05 tags: 传送门 题目大意 解题思路 行的感兴趣的摊点或者列的感兴趣的摊点的数量能被行数或者列数整除&#xff0c;则能够实现要求。“均分”思想&#xff0c;设总感兴趣摊点数 T T T 和行数列数 n n n&#xff0c;当前感兴趣的摊点数…

三、HTML元素

一、HTML元素 HTML 文档由 HTML 元素定义。 *开始标签常被称为起始标签&#xff08;opening tag&#xff09;&#xff0c;结束标签常称为闭合标签&#xff08;closing tag&#xff09;。 二、HTML 元素语法 HTML 元素以开始标签起始。HTML 元素以结束标签终止。元素的内容是…