Vue3 编译原理

文章目录

  • 一、编译流程
    • 1. 解读入口文件 packgages/vue/index.ts
    • 2. compile函数的运行流程
  • 二、AST 解析器
    • 1. `ast` 的生成
    • 2. 创建`ast`的根节点
    • 3. 解析子节点 `parseChildren`(关键)
    • 4. 解析模版元素 Element
    • 模版元素解析-举例分析

一、编译流程

1. 解读入口文件 packgages/vue/index.ts

首先从Vue对象的入口开始,packgages/vue/index.ts文件中只有compileToFunction函数:

  1. 依赖注入编译函数至runtimeregisterRuntimeCompiler(compileToFunction)
  2. runtime 调用编译函数compileToFunction
  3. 返回包含code的编译结果
  4. 将code作为参数传入Function 的构造函数将生成函数赋值给render变量。
  5. 将render函数作为编译结果返回
    在这里插入图片描述
    下面这个简单的模版,
<template><div>Hello World</div>
</template>

经过编译后,code返回的字符串为:

const _Vue = Vue return function render(_ctx, _cache) {with(_ctx) {const {openBlock: _openBlock, createBlock:_createBlock} = _Vue;return (_openBlock(), _createBlock("div", null, "Hello World"))	}
}
  • 拿到这个代码字符串的结果后,第25行声明了一个render变量,并将生成的代码字符串code 作为参数传入了new Function 构造函数,生成了render函数。可以将上面的code字符串格式化。
  • 这里的render显而易见是一个柯里化的函数,返回了一个函数,函数内部通过with来扩展作用域链。
  • 最后,入口文件返回了render变量,并顺手缓存了render函数。
  • 在第一行,入口文件创建了一个compileCache 对象,用以缓存compileToFunction 函数生成的render 函数,将template 参数作为缓存的key,并在11行进行if分支做缓存判断,如果该模版之前被缓存过,则不再进行编译,直接返回缓存中的render函数,以此提高性能。

2. compile函数的运行流程

compile函数涉及到compile-domcompile-core 两个模块。
compile的运行流程:

  1. baseCompile命名理由:因为compile-core是编译的核心模块,接收外部的参数来按照规则完成编译,而compile-dom是专门处理浏览器场景下的编译,在这个模块下导出的compile函数是入口文件真正接收的编译函数。而compile-dom中的compile函数相对baseCompile也是一个更高阶的编译器。例如:当Vue在weex或iOS或Android这些Native App中工作时,compile-dom可能会被相关的移动端编译库来取代。
  2. baseCompile函数:
    在这里插入图片描述
  • 从函数声明中看,baseCompile接收template模版以及上层高阶编译器处理过的options编译选项,最终返回一个CodegenResult类型的编译结果。
export interface CodegenResult {code: stringpreamble: stringast: RootNodemap?: RawSourceMap
}
  • 看上方源码的第12行,判断template模版是否为字符串,如果是的话,则会对字符串进行解析,否则直接将template作为AST。(我们平时写的vue代码都是以字符串的形式传递进去的。)
  • 然后是第16行调用了transform函数,以及传入了指令转换、节点等工具函数,对由模版生成的AST进行转换。
  • 最后32行,将转换好的ast传入进generate,生成CodegenResult类型的返回结果。

二、AST 解析器

1. ast 的生成

ast的生成有一个三目运算符的判断,如果传进来的template模版是一个字符串,那么则调用baseParse解析模版字符串,否则直接将template作为ast对象。

baseParse 函数:

export function baseParse(content: string,options: ParserOptions = {}
): RootNode {const context = createParserContext(content, options) // 创建解析的上下文对象const start = getCursor(context) // 生成记录解析过程的游标信息return createRoot( // 生成并返回 root 根节点parseChildren(context, TextModes.DATA, []), // 解析子节点,作为 root 根节点的 children 属性getSelection(context, start))
}
  • 首先会创建解析的上下文,根据上下文获取游标信息,由于还未进行解析,所以游标中的columnlineoffset属性对应的都是template的起始位置。
  • 之后就是创建根节点,并返回根节点,至此ast树生成,解析完成。

2. 创建ast的根节点

export function createRoot(children: TemplateChildNode[],loc = locStub
): RootNode {return {type: NodeTypes.ROOT,children,helpers: [],components: [],directives: [],hoists: [],imports: [],cached: 0,temps: 0,codegenNode: undefined,loc}
}
  • 该函数返回了一个RootNode类型的根节点对象,其中我们传入的children参数会被作为根节点的children参数。

3. 解析子节点 parseChildren(关键)

function parseChildren(context: ParserContext,mode: TextModes,ancestors: ElementNode[]
): TemplateChildNode[] {const parent = last(ancestors) // 获取当前节点的父节点const ns = parent ? parent.ns : Namespaces.HTMLconst nodes: TemplateChildNode[] = [] // 存储解析后的节点// 当标签未闭合时,解析对应节点while (!isEnd(context, mode, ancestors)) {/* 忽略逻辑 */}// 处理空白字符,提高输出效率let removedWhitespace = falseif (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {/* 忽略逻辑 */}// 移除空白字符,返回解析后的节点数组return removedWhitespace ? nodes.filter(Boolean) : nodes
}
  • parseChildren函数接收三个参数,context解析器上下文,mode文本数据类型,ancestors祖先节点数据。
  • 函数执行首先会从祖先节点中获取当前节点的父节点,确定命名空间,以及创建一个空数组,用来存储解析后的节点。
  • 之后会有一个while循环,判断是否到达了标签的关闭位置,如果不是需要关闭的标签,则在循环体内对源模版字符串进行分类解析。
  • 之后会有一段处理空白字符的逻辑,处理完成后返回解析好的nodes数组。

while循环内的逻辑(函数的核心)

  • 在while中会判断文本数据的类型,只有当TextModes为DATA或RCDATA时会继续往下解析。
    • 第一种情况就是判断是否需要解析Vue模版语法中的Mustache语法,如果当前上下文中没有v-pre指令来跳过表达式,并且源模版字符串是以我们指定的分隔符开头的,就会进行双大括号的解析。
    • 接下来会判断,如果第一个字符是<并且第二个字符是! ,会尝试解析注释标签,<!DOCTYPE><!CDATA这三种情况,对于DOCTYPE会进行忽略,解析成注释。
    • 之后会判断当第二个字符是/的情况,</已经满足了一个闭合标签的条件了,所以会尝试匹配闭合标签。当第三个标签是>,缺少了标签名字,会报错,并让解析器的进度前进三个字符,跳过</>
    • 如果是</,并且第三个字符是小写英文字符,解析器会解析结束标签。
    • 如果源模版字符串的第一个字符是<,第二个字符是小写英文字符开头,会调用parseElement函数来解析对应的标签。
    • 当这个判断字符串字符的分支条件结束,并且没有解析出任何node节点,则会将node作为文本类型,调用parseText进行解析。
    • 最后将生成的节点添加进nodes数组,在函数结束时返回。

while循环的源码如下:

while (!isEnd(context, mode, ancestors)) {const s = context.sourcelet node: TemplateChildNode | TemplateChildNode[] | undefined = undefinedif (mode === TextModes.DATA || mode === TextModes.RCDATA) {if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {/* 如果标签没有 v-pre 指令,源模板字符串以双大括号 `{{` 开头,按双大括号语法解析 */node = parseInterpolation(context, mode)} else if (mode === TextModes.DATA && s[0] === '<') {// 如果源模板字符串的第以个字符位置是 `!`if (s[1] === '!') {// 如果以 '<!--' 开头,按注释解析if (startsWith(s, '<!--')) {node = parseComment(context)} else if (startsWith(s, '<!DOCTYPE')) {// 如果以 '<!DOCTYPE' 开头,忽略 DOCTYPE,当做伪注释解析node = parseBogusComment(context)} else if (startsWith(s, '<![CDATA[')) {// 如果以 '<![CDATA[' 开头,又在 HTML 环境中,解析 CDATAif (ns !== Namespaces.HTML) {node = parseCDATA(context, ancestors)}}// 如果源模板字符串的第二个字符位置是 '/'} else if (s[1] === '/') {// 如果源模板字符串的第三个字符位置是 '>',那么就是自闭合标签,前进三个字符的扫描位置if (s[2] === '>') {emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2)advanceBy(context, 3)continue// 如果第三个字符位置是英文字符,解析结束标签} else if (/[a-z]/i.test(s[2])) {parseTag(context, TagType.End, parent)continue} else {// 如果不是上述情况,则当做伪注释解析node = parseBogusComment(context)}// 如果标签的第二个字符是小写英文字符,则当做元素标签解析} else if (/[a-z]/i.test(s[1])) {node = parseElement(context, ancestors)// 如果第二个字符是 '?',当做伪注释解析} else if (s[1] === '?') {node = parseBogusComment(context)} else {// 都不是这些情况,则报出第一个字符不是合法标签字符的错误。emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)}}}// 如果上述的情况解析完毕后,没有创建对应的节点,则当做文本来解析if (!node) {node = parseText(context, mode)}// 如果节点是数组,则遍历添加进 nodes 数组中,否则直接添加if (isArray(node)) {for (let i = 0; i < node.length; i++) {pushNode(nodes, node[i])}} else {pushNode(nodes, node)}
}

4. 解析模版元素 Element

parseElement精简源码如下:

function parseElement(context: ParserContext,ancestors: ElementNode[]
): ElementNode | undefined {// 解析起始标签const parent = last(ancestors)const element = parseTag(context, TagType.Start, parent)// 如果是自闭合的标签或者是空标签,则直接返回。voidTag例如: `<img>`, `<br>`, `<hr>`if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {return element}// 递归的解析子节点ancestors.push(element)const mode = context.options.getTextMode(element, parent)const children = parseChildren(context, mode, ancestors)ancestors.pop()element.children = children// 解析结束标签if (startsWithEndTagOpen(context.source, element.tag)) {parseTag(context, TagType.End, parent)} else {emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {const first = children[0]if (first && startsWith(first.loc.source, '<!--')) {emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)}}}// 获取标签位置对象element.loc = getSelection(context, element.loc.start)return element
}
  • 首先会获取当前节点的父节点,再调用parseTag()函数解析。
    parseTag()函数执行流程:
    • 匹配标签名
    • 解析元素中的attribute属性,存储至props属性
    • 检测是否存在v-pre属性,如果存在,则修改context上下文中的inVPre属性为true。
    • 检测自闭合标签,如果是自闭合,则将isSelfClosing属性置为true。
    • 判断tagType,是Element还是component组件,或slot插槽。
    • 返回生成的element对象
  • 获取到 element对象后,会判断element是否是自闭合标签,或空标签,例如<img><br><hr>,如果是这种情况,直接返回element对象。
  • 然后解析element的子节点,把element压入栈中,然后递归调用parseChildren来解析子节点。

const parent = last(ancestors)
在将element入栈后,拿到的父节点就是当前节点。

  • 解析完毕后,调用ancestors.pop(),让当前解析完子节点的element对象出栈,将解析后的children对象赋值给element的children属性,完成element的子节点解析。
  • 最后匹配结束标签,设置element的Ioc位置信息,返回解析完毕的 element 对象。

模版元素解析-举例分析

<div><p>Hello World</p>
</div>

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

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

相关文章

嵌入式养成计划-33--数据库-sqlite3

七十一、 数据库 71.1 数据库基本概念 数据&#xff08;Data&#xff09; 能够输入计算机并能被计算机程序识别和处理的信息集合数据库 &#xff08;Database&#xff09;数据库是在数据库管理系统管理和控制之下&#xff0c;存放在存储介质上的数据集合 常用的数据库 大型数…

第二证券:5.5G时代将至 算力基建迎政策助力

昨日&#xff0c;A股全线低开&#xff0c;三大股指盘中均跌超1%&#xff0c;盘中冲高回落&#xff0c;午后逐渐止跌。到收盘&#xff0c;沪指跌0.44%报3096.92点&#xff0c;深成指微跌0.03%报10106.96点&#xff0c;创业板指跌0.26%报1998.61点&#xff0c;两市算计成交7700元…

【unity】制作一个角色的初始状态(左右跳二段跳)【2D横板动作游戏】

前言 hi~ 大家好&#xff01;欢迎大家来到我的全新unity学习记录系列。现在我想在2d横板游戏中&#xff0c;实现一个角色的初始状态-闲置状态、移动状态、空中状态。并且是利用状态机进行实现的。 本系列是跟着视频教程走的&#xff0c;所写也是作者个人的学习记录笔记。如有错…

linux centos出现No space left on device解决方案

问题是因为系统磁盘空间不足 解决方法: 找到那个磁盘不足问题 df -lh 发现/dev/mapper/cl-root磁盘已用50G,有如下 解决方案&#xff1a; 1、如果是虚拟机可以通过分配空间使其空间增加 2、将其他不常用磁盘空间分配给cl-root如&#xff08; /dev/mapper/cl-home &#…

unity 使用模拟器进行Profiler性能调试

这篇文章主要记录如何实现通过模拟器对打包的app游戏进行Profiler调试。主要记录一些比较重要的点。 准备工作 首先你要能够打包unity的安卓包&#xff0c;如果没有安装安卓组件&#xff0c;请先安装组件。 安装完成以后&#xff0c;会在unity的安装目录找到相应的SDK 这个…

嵌入式Linux裸机开发(五)中断管理

系列文章目录 文章目录 系列文章目录前言STM32 中断系统IMX6U中断控制8个中断GIC中断控制器GIC介绍中断IDGIC逻辑分块GIC协处理器 中断使能中断优先级 重点代码分析官方SDK函数start.S文件自行编写中断驱动文件 前言 最近在学习中发现&#xff0c;学Linux嵌入式不仅是对Linux的…

为Yolov7环境安装Cuba匹配的Pytorch

1. 查看Cuba版本 方法一 nvidia-smi 找到CUDA Version 方法二 Nvidia Control Panel > 系统信息 > 组件 > 2. 安装Cuba匹配版本的PyTorch https://pytorch.org/get-started/locally/这里使用conda安装 conda install pytorch torchvision torchaudio pytorch-cu…

JDK、JRE、JVM三者之间的关系

1.JDK 基本介绍 1) JDK 的全称 (Java Development Kit Java 开发工具包 ) JDK JRE java 的开发工具 [java, javac,javadoc,javap 等 ] 2) JDK 是提供给 Java 开发人员使用的&#xff0c;其中包含了 java 的开发工具&#xff0c;也包括了 JRE 。所以安装了 JDK &#xff0c;就…

论文研读|Protecting Intellectual Property of Deep Neural Networks with Watermarking

目录 论文信息文章简介研究动机研究方法水印生成水印嵌入版权验证 实验结果有效性&#xff08;Effectiveness&#xff09;高效性&#xff08;Converge Speed&#xff09;保真度&#xff08;Functionality&#xff09;鲁棒性&#xff08;Robustness&#xff09;Anti-剪枝攻击&am…

SpringBoot源码分析-自动装配-实现原理

文章目录 SpringBoot自动装配前言介绍实现原理SpringBootApplicationEnableAutoConfigurationselectImports方法没有走&#xff1f;DeferredImportSelector源码分析设计目的 总结 SpringBoot自动装配 前言 什么是自动装配&#xff1f;用过Spring的应该都知道&#xff0c;虽然…

MySQL总结练习题

目录 1.准备数据表 2.表之间的关系 3.题目 3.1 取得每个部门最高薪水的人员名称 3.2 哪些人的薪水在部门的平均薪水之上 3.3 取得部门中&#xff08;所有人的&#xff09;平均的薪水等级 3.4 不准用组函数&#xff08;Max &#xff09;&#xff0c;取得最高薪水 3.5 取…

【数据结构】归并排序和计数排序(排序的总结)

目录 一&#xff0c;归并排序的递归 二&#xff0c;归并排序的非递归 三&#xff0c;计数排序 四&#xff0c;排序算法的综合分析 一&#xff0c;归并排序的递归 基本思想&#xff1a; 归并采用的是分治思想&#xff0c;是分治法的一个经典的运用。该算法先将原数据进行拆…

BUUCTF SimpleRev

分析 该文件为64位的ELF文件&#xff0c;运行在linux平台 使用IDA64打开 进入Decry函数 输入flag和成功的提示 看看如何才能成功拿到flag 这里比较text和str2&#xff0c;text是源代码就有的 那么str2应该就是我们输入的内容 先分析text的内容是什么 进入join函数 该函数…

SpringBoot项目整合MybatisPlus持久层框架+Druid数据库连接池

前言 之前搭建SpringBoot项目工程&#xff0c;所使用的持久层框架不是Mybatis就是JPA&#xff0c;还没试过整合MybatisPlus框架并使用&#xff0c;原来也如此简单。在此简单记录一下在SpringBoot项目中&#xff0c;整合MybatisPlus持久层框架、Druid数据库连接池的过程。 一、…

Eclipse iceoryx(千字自传)

1 在固定时间内实现无任何限制的数据传输 在汽车automotive、机器人robotics和游戏gaming等领域,必须在系统的不同部分之间传输大量数据。使用Linux等操作系统时,必须使用进程间通信(IPC)机制传输数据。Eclipse iceoryx是一种中间件,它使用零拷贝Zero-Copy、共享内存Share…

【OSPF宣告——network命令与多区域配置实验案例】

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大二在校生&#xff0c;喜欢编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;小新爱学习. &#x1f43c;个人WeChat&#xff1a;hmmwx53 &#x1f54a;️系列专栏&#xff1a;&#x1f5bc…

win10电脑插入耳机,右边耳机声音比左边小很多

最近使用笔记本看视频&#xff0c;发现插入耳机&#xff08;插入式和头戴式&#xff09;后&#xff0c;右边耳机声音比左边耳机声音小很多很多&#xff0c;几乎是一边很清晰&#xff0c;另一边什么都听不到。 将耳机插到别人电脑上测试耳机正常&#xff0c;那就是电脑的问题。试…

自然语言处理(NLP)的开发框架

自然语言处理&#xff08;NLP&#xff09;领域有许多开源的框架和库&#xff0c;用于处理文本数据和构建NLP应用程序。以下是一些常见的NLP开源框架及其特点&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合…

Outlook屏蔽Jira AI提醒

前言&#xff1a;最近不知道为什么jira上的ai小助手抽风&#xff0c;一周发个几千封邮件…导致我现在都不想在邮箱里面跟找垃圾一样找消息了。实在忍无可忍&#xff0c;决定屏蔽AI小助手&#xff0c;方法很简单&#xff0c;follow me~~ 第一步&#xff1a;双击打开电脑版Outloo…

springboot家乡特色推荐系统springboot28

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…