[Vue] Vue模板编译原理解析 part3

news/2025/4/1 21:36:08/文章来源:https://www.cnblogs.com/Answer1215/p/18801251

生成 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.hqwc.cn/news/908367.html

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

相关文章

关于window版本nacos版本安装过程

环境 window10 16G 1TB下载地址window版本zip包 https://github.com/alibaba/nacos/releases/download/2.5.1/nacos-server-2.5.1.zip Linux版tar包 https://github.com/alibaba/nacos/releases/download/2.5.1/nacos-server-2.5.1.tar.gz安装前提安装jdk 1.8及以上,我安装了…

Netty源码—10.Netty工具之时间轮

大纲 1.什么是时间轮 2.HashedWheelTimer是什么 3.HashedWheelTimer的使用 4.HashedWheelTimer的运行流程 5.HashedWheelTimer的核心字段 6.HashedWheelTimer的构造方法 7.HashedWheelTimer添加任务和执行任务 8.HashedWheelTimer的完整源码 9.HashedWheelTimer的总结 10.Hashe…

0330-好的开始是成功的一半

前言 今天帮一个 USC Game Dev 专业同学做项目,真的挺复杂的一个项目。 但是我依然把项目配置好了,后面就是慢慢的添加新功能。 我用了 git 管理这个项目,把自己的每一步关键操作都用 git commit 记录一下。效果心路历程 我想过很多次 “要不就放弃吧” 但是看到旁边的“Att…

数仓项目建设方案——维度建模

数仓项目建设方案——维度建模式信息收集项目背景 阐述公司当前的行业,涉及的主要业务,相关数据的大小、分布、更新情况描述,需要解决的相关问题。公司当前数据建设现状 使用的数据库、数据来源系统与方式、现有数据分析组织,所使用的 BI 工具与数仓工具、为什么建立以及当…

在机器人和无人机时代,测绘人的出路在哪里?

一、技术革命:当测绘行业按下“加速键”无人机与机器人技术正在重塑测绘行业的底层逻辑。传统测绘依赖人工作业,效率低、成本高且风险大,而无人机凭借其灵活性和高效性,已能快速完成大范围地形测绘,精度可达厘米级,甚至替代人工进入危险区域(如塌方、悬崖等)作业。例如…

openwrt禁止设备联网

一、代码操作 把mac地址换成要禁用的设备mac地址,加到自定义防火墙最后,记得最后重启防火墙生效 /etc/init.d/firewall restart iptables -I INPUT -m mac --mac-source B8:C7:4A:7A:66:2E -j DROP iptables -I FORWARD -m mac --mac-source B8:C7:4A:7A:66:2E -j DROP iptab…

JVM调优原理篇

JVM调优 什么是JVM调优,调优的指标是什么? JVM调优指的就是对当前系统进行性能调优,简单来说就是尽可能使用较小的内存和CPU来让JAVA程序获得更高的吞吐量及较低的延迟。 调优常见的指标:吞吐量:是指不考虑垃圾收集引起的停顿时间或内存消耗,应用达到的最高性能指标。 延…

20241216 实验二《Python程序设计》实验报告

20241216 2024-2025-2 《Python程序设计》实验二报告 课程:《Python程序设计》 班级: 2412 姓名: 曾楷 学号:20241216 实验教师:王志强 实验日期:2024年3月26日 必修/选修: 公选课 (一) 实验内容 1.设计并完成一个完整的应用程序,完成加减乘除模等运算,功能多多益善…

[Windows] TechSweeper 应用程序卸载神器V1.2.1

一.我们改进了程序元素显示,现在超出列宽,可以进行滚动显示二.我们为程序添加了右键菜单,现在功能更加全面三.现在程序出现崩溃时,可以进行错误提示与收集四.现在程序可以进行主题切换了五.添加了作者相关信息六.现在可以打开程序相关注册表了(直接显示 清晰明了)七.现在可…

鸢尾花书 - Book_3《数学要素》 - Chapter1 万物皆数

上面图片摘自原书 一、基础概念普及 1. 向量 若干数字排成一行或一列,并且用中括号括起来,得到的数组叫做向量。 2. 行向量 排成一行 3. 列向量 排成一列 4. 转置 行向量转置得到列向量,反之。 5. 矩阵 有行,有列,像表格。 6. 元素 x[i][j] 代表矩阵 X 中第 i 行第 j 列元…

VMware workstation 17 pro 设置开机自启虚拟机(Windows 11)

首先在软件界面设置需要启动的虚拟机 文件➡配置自动启动的虚拟机在打开的对话框中勾选需要自动启动的虚拟机和设置启动顺序点击确定即可。如果点击确定报错的话,如下图看起来问题可能出在两个方面:要么是vmAutoStart.xml文件不存在,要么是当前用户没有足够的权限去修改这个…

RabbitMQ进阶--集群,分布式事务

一.RabbitMQ集群搭建 RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering。这使得RabbitMQ本身不需要像ActiveMQ、Kafka那样通过ZooKeeper分别来实现HA方…