[Vue] Vue 模板编译原理解析 part 2

news/2025/4/1 6:39:09/文章来源:https://www.cnblogs.com/Answer1215/p/18799809

转换器

主要的目的是将模板的 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 中间件采用的洋葱模型

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

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

相关文章

Flasher V5 JLink Pro V6

原帖链接:https://nicemcu.github.io/2025/03/29/PSoC4/FlasherV5/ 2025年3月29日,神变月最后一天,我们参加了放生~ 在这个特殊的日子里,我完成了Flasher V5和JLink Pro V6的crack,难掩内心的喜悦与激动,记录下这一时刻。 前不久小黄鱼上收了一枚Flasher ARM,硬件版本V5…

[Vue] Vue模板编译原理解析 part 1

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

[P] 结对项目:影蛇舞

项目 内容这个作业属于哪个课程 2025年春季软件工程(罗杰、任健) 这个作业的要求在哪里 [P] 结对项目:影蛇舞 我在这个课程的目标是 学习软件工程知识,通过团队协作开发一个具备实际应用价值的软件,从需求分析、设计、开发到测试和部署,完整经历软件开发生命周期,提高工…

0329-Never Give Up

前言 用LCD1602 去打印一段话吧。 就当作激励和安慰,毕竟,我今天被坏情绪传染了一下,有点点失落和悲伤。 参考 5-2 LCD1602调试器 效果 Never Give UpAttitude Is Everything代码 LCD1602.h #ifndef __LCD1602_H__ #define __LCD1602_H__//用户调用函数: void LCD_Init(); …

JavaScript 数据结构与算法 — 单向链表

链表(Linked List)是一种基本的数据结构,用于表示一组按顺序排列的元素。链表中的每个元素都与下一个元素连接,元素在内存中并不是连续的,而是通过指针来链接在一起。每个元素都包含两部分:自己的数据和指向下一个元素的指针。我们常说的链表指的是单向链表,第一个元素的…

Ethernaut通关(智能合约漏洞)(有缘更新)

SnowSword笑传之出错币Ethernaut通关 参考文章:文章 - Ethernaut闯关录(上) - 先知社区、 智能合约是什么?把智能合约想象成网络上的赛博自动售货机,每个人都可以写自己的智能合约,使用虚拟货币交易物品,并且网络区块链中的所有节点都在为你的交易记账不怕商家提桶跑路……

QML基本组件 滑动条 Slider

描述 Slider通过手柄沿轨迹滑动来设置数值。 Qt帮助文档搜索 “slider” 获取详细信息。属性 from : real to : real value : real orientation : enumeration stepsize : real touchDragThreshold : qreal信号 onValueChange {}import QtQuick import QtQuick.ControlsWindow …

UE5--006--小结(一)

1. Input2. AI Enmy3.Save Game

阶段升级,zhitan-ems开源能源管理系统--集成建筑能耗支路和分项功能

升级介绍 自从春节上班后开源以来,zhitan-ems收到了大家很多的赞誉和认可,很多朋友也提出了中肯的意见。感谢大家。 很多朋友的建议里提到建筑能耗功能,依据大家意见,我们加班加点实现了简单的建筑能耗功能。如下图: 另外打一波广告,欢迎大家star 项目介绍 通过物联网技…

Static Timing Analysis Basics

Preface This note only introduce the essential concepts about Static Timing Analysis, which not contains:Async, i.e. remove, recover Timing conceptions, i.e. false path, multi cycle path etc. Advance timing domain knowledgePOCV, MCMM etc.什么是 STA 由于时钟…

深圳大学的一些简单题

A打表,发现是这样的东西:然后规律很显然,相邻的两个数,一组在左边,另一组在右边,依次循环,偶数的时候是 \(23\) 开头,奇数的时候是 \(12\) 开头,再处理一下 \(1\) 和 \(n\) 就可以,比较简单的分讨 显然规律不止一个点击查看代码 #include <bits/stdc++.h> usin…

AI可解释性 I | 对抗样本(Adversarial Sample)论文导读(持续更新)

本文作为AI可解释性系列的第一部分,旨在以汉语整理并阅读对抗攻击(Adversarial Attack)相关的论文,并持续更新。与此同时,AI可解释性系列的第二部分:归因方法(Attribution)也即将上线,敬请期待。AI可解释性 I | 对抗样本(Adversarial Sample)论文导读(持续更新) 导…