Vue源码系列讲解——模板编译篇【四】(文本解析器)

1. 前言

在上篇文章中我们说了,当HTML解析器解析到文本内容时会调用4个钩子函数中的chars函数来创建文本型的AST节点,并且也说了在chars函数中会根据文本内容是否包含变量再细分为创建含有变量的AST节点和不包含变量的AST节点,如下:

// 当解析到标签的文本时,触发chars
chars (text) {if(res = parseText(text)){let element = {type: 2,expression: res.expression,tokens: res.tokens,text}} else {let element = {type: 3,text}}
}

从上面代码中可以看到,创建含有变量的AST节点时节点的type属性为2,并且相较于不包含变量的AST节点多了两个属性:expressiontokens。那么如何来判断文本里面是否包含变量以及多的那两个属性是什么呢?这就涉及到文本解析器了,当VueHTML解析器解析出文本时,再将解析出来的文本内容传给文本解析器,最后由文本解析器解析该段文本里面是否包含变量以及如果包含变量时再解析expressiontokens。那么接下来,本篇文章就来分析一下文本解析器都干了些什么。

2. 结果分析

研究文本解析器内部原理之前,我们先来看一下由HTML解析器解析得到的文本内容经过文本解析器后输出的结果是什么样子的,这样对我们后面分析文本解析器内部原理会有很大的帮助。

从上面chars函数的代码中可以看到,把HTML解析器解析得到的文本内容text传给文本解析器parseText函数,根据parseText函数是否有返回值判断该文本是否包含变量,以及从返回值中取到需要的expressiontokens。那么我们就先来看一下parseText函数如果有返回值,那么它的返回值是什么样子的。

假设现有由HTML解析器解析得到的文本内容如下:

let text = "我叫{{name}},我今年{{age}}岁了"

经过文本解析器解析后得到:

let res = parseText(text)
res = {expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",tokens:["我叫",{'@binding': name },",我今年"{'@binding': age },"岁了"]
}

从上面的结果中我们可以看到,expression属性就是把文本中的变量和非变量提取出来,然后把变量用_s()包裹,最后按照文本里的顺序把它们用+连接起来。而tokens是个数组,数组内容也是文本中的变量和非变量,不一样的是把变量构造成{'@binding': xxx}

那么这样做有什么用呢?这主要是为了给后面代码生成阶段的生成render函数时用的,这个我们在后面介绍代码生成阶段是会详细说明,此处暂可理解为单纯的在构造形式。

OK,现在我们就可以知道文本解析器内部就干了三件事:

  • 判断传入的文本是否包含变量
  • 构造expression
  • 构造tokens

那么接下来我们就通过阅读源码,逐行分析文本解析器内部工作原理。

3. 源码分析

文本解析器的源码位于 src/compiler/parser/text-parsre.js 中,代码如下:

const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const buildRegex = cached(delimiters => {const open = delimiters[0].replace(regexEscapeRE, '\\$&')const close = delimiters[1].replace(regexEscapeRE, '\\$&')return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})
export function parseText (text,delimiters) {const tagRE = delimiters ? buildRegex(delimiters) : defaultTagREif (!tagRE.test(text)) {return}const tokens = []const rawTokens = []/*** let lastIndex = tagRE.lastIndex = 0* 上面这行代码等同于下面这两行代码:* tagRE.lastIndex = 0* let lastIndex = tagRE.lastIndex*/let lastIndex = tagRE.lastIndex = 0let match, index, tokenValuewhile ((match = tagRE.exec(text))) {index = match.index// push text tokenif (index > lastIndex) {// 先把'{{'前面的文本放入tokens中rawTokens.push(tokenValue = text.slice(lastIndex, index))tokens.push(JSON.stringify(tokenValue))}// tag token// 取出'{{ }}'中间的变量expconst exp = parseFilters(match[1].trim())// 把变量exp改成_s(exp)形式也放入tokens中tokens.push(`_s(${exp})`)rawTokens.push({ '@binding': exp })// 设置lastIndex 以保证下一轮循环时,只从'}}'后面再开始匹配正则lastIndex = index + match[0].length}// 当剩下的text不再被正则匹配上时,表示所有变量已经处理完毕// 此时如果lastIndex < text.length,表示在最后一个变量后面还有文本// 最后将后面的文本再加入到tokens中if (lastIndex < text.length) {rawTokens.push(tokenValue = text.slice(lastIndex))tokens.push(JSON.stringify(tokenValue))}// 最后把数组tokens中的所有元素用'+'拼接起来return {expression: tokens.join('+'),tokens: rawTokens}
}

我们看到,除开我们自己加的注释,代码其实不复杂,我们逐行分析。

parseText函数接收两个参数,一个是传入的待解析的文本内容text,一个包裹变量的符号delimiters。第一个参数好理解,那第二个参数是干什么的呢?别急,我们看函数体内第一行代码:

const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE

函数体内首先定义了变量tagRE,表示一个正则表达式。这个正则表达式是用来检查文本中是否包含变量的。我们知道,通常我们在模板中写变量时是这样写的:hello 。这里用{{}}包裹的内容就是变量。所以我们就知道,tagRE是用来检测文本内是否有{{}}。而tagRE又是可变的,它是根据是否传入了delimiters参数从而又不同的值,也就是说如果没有传入delimiters参数,则是检测文本是否包含{{}},如果传入了值,就会检测文本是否包含传入的值。换句话说在开发Vue项目中,用户可以自定义文本内包含变量所使用的符号,例如你可以使用%包裹变量如:hello %name%。

接下来用tagRE去匹配传入的文本内容,判断是否包含变量,若不包含,则直接返回,如下:

if (!tagRE.test(text)) {return
}

如果包含变量,那就继续往下看:

const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {}

接下来会开启一个while循环,循环结束条件是tagRE.exec(text)的结果match是否为nullexec( )方法是在一个字符串中执行匹配检索,如果它没有找到任何匹配就返回null,但如果它找到了一个匹配就返回一个数组。例如:

tagRE.exec("hello {{name}},I am {{age}}")
//返回:["{{name}}", "name", index: 6, input: "hello {{name}},I am {{age}}", groups: undefined]
tagRE.exec("hello")
//返回:null

可以看到,当匹配上时,匹配结果的第一个元素是字符串中第一个完整的带有包裹的变量,第二个元素是第一个被包裹的变量名,第三个元素是第一个变量在字符串中的起始位置。

接着往下看循环体内:

while ((match = tagRE.exec(text))) {index = match.indexif (index > lastIndex) {// 先把'{{'前面的文本放入tokens中rawTokens.push(tokenValue = text.slice(lastIndex, index))tokens.push(JSON.stringify(tokenValue))}// tag token// 取出'{{ }}'中间的变量expconst exp = match[1].trim()// 把变量exp改成_s(exp)形式也放入tokens中tokens.push(`_s(${exp})`)rawTokens.push({ '@binding': exp })// 设置lastIndex 以保证下一轮循环时,只从'}}'后面再开始匹配正则lastIndex = index + match[0].length}

上面代码中,首先取得字符串中第一个变量在字符串中的起始位置赋给index,然后比较indexlastIndex的大小,此时你可能有疑问了,这个lastIndex是什么呢?在上面定义变量中,定义了let lastIndex = tagRE.lastIndex = 0,所以lastIndex就是tagRE.lastIndex,而tagRE.lastIndex又是什么呢?当调用exec( )的正则表达式对象具有修饰符g时,它将把当前正则表达式对象的lastIndex属性设置为紧挨着匹配子串的字符位置,当同一个正则表达式第二次调用exec( ),它会将从lastIndex属性所指示的字符串处开始检索,如果exec( )没有发现任何匹配结果,它会将lastIndex重置为0。示例如下:

const tagRE = /\{\{((?:.|\n)+?)\}\}/g
tagRE.exec("hello {{name}},I am {{age}}")
tagRE.lastIndex   // 14

从示例中可以看到,tagRE.lastIndex就是第一个包裹变量最后一个}所在字符串中的位置。lastIndex初始值为0。

那么接下里就好理解了,当index>lastIndex时,表示变量前面有纯文本,那么就把这段纯文本截取出来,存入rawTokens中,同时再调用JSON.stringify给这段文本包裹上双引号,存入tokens中,如下:

if (index > lastIndex) {// 先把'{{'前面的文本放入tokens中rawTokens.push(tokenValue = text.slice(lastIndex, index))tokens.push(JSON.stringify(tokenValue))
}

如果index不大于lastIndex,那说明index也为0,即该文本一开始就是变量,例如:hello。那么此时变量前面没有纯文本,那就不用截取,直接取出匹配结果的第一个元素变量名,将其用_s()包裹存入tokens中,同时再把变量名构造成{'@binding': exp}存入rawTokens中,如下:

// 取出'{{ }}'中间的变量exp
const exp = match[1].trim()
// 把变量exp改成_s(exp)形式也放入tokens中
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })

接着,更新lastIndex以保证下一轮循环时,只从}}后面再开始匹配正则,如下:

lastIndex = index + match[0].length

接着,当while循环完毕时,表明文本中所有变量已经被解析完毕,如果此时lastIndex < text.length,那就说明最后一个变量的后面还有纯文本,那就将其再存入tokensrawTokens中,如下:

// 当剩下的text不再被正则匹配上时,表示所有变量已经处理完毕
// 此时如果lastIndex < text.length,表示在最后一个变量后面还有文本
// 最后将后面的文本再加入到tokens中
if (lastIndex < text.length) {rawTokens.push(tokenValue = text.slice(lastIndex))tokens.push(JSON.stringify(tokenValue))
}

最后,把tokens数组里的元素用+连接,和rawTokens一并返回,如下:

return {expression: tokens.join('+'),tokens: rawTokens
}

以上就是文本解析器parseText函数的所有逻辑了。

4. 总结

本篇文章介绍了文本解析器的内部工作原理,文本解析器的作用就是将HTML解析器解析得到的文本内容进行二次解析,解析文本内容中是否包含变量,如果包含变量,则将变量提取出来进行加工,为后续生产render函数做准备。

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

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

相关文章

如何在30天内使用python制作一个卡牌游戏

如何在30天内使用python制作一个卡牌游戏 第1-5天&#xff1a;规划和设计第6-10天&#xff1a;搭建游戏框架第11-20天&#xff1a;核心游戏机制开发第21-25天&#xff1a;游戏界面和用户体验第26-30天&#xff1a;测试和发布附加建议游戏类型游戏规则设计界面设计技术选型第6-…

LeetCode918. Maximum Sum Circular Subarray——动态规划

文章目录 一、题目二、题解 一、题目 Given a circular integer array nums of length n, return the maximum possible sum of a non-empty subarray of nums. A circular array means the end of the array connects to the beginning of the array. Formally, the next el…

Flutter run 一直 Running Gradle task ‘assembleDebug’…

发生缘由 Flutter 项目引入 fluttertoast 插件后&#xff0c;执行 Flutter run 一直 Running Gradle task ‘assembleDebug’…&#xff0c;最后发现下载 kotlin-compiler-embeddable-7.1.0.jar 特别的缓慢。 运行环境 电脑系统版本&#xff1a;Windows 10 64bit VS Code&…

【Qt】环境安装与初识

目录 一、Qt背景介绍 二、搭建Qt开发环境 三、新建工程 四、Qt中的命名规范 五、Qt Creator中的快捷键 六、QWidget基础项目文件详解 6.1 .pro文件解析 6.2 widget.h文件解析 6.3 widget.cpp文件解析 6.4 widget.ui文件解析 6.5 main.cpp文件解析 七、对象树 八、…

垃圾分类|城市垃圾分类管理系统|基于Springboot的城市垃圾分类管理系统设计与实现(源码+数据库+文档)

城市垃圾分类管理系统目录 目录 基于Springboot的城市垃圾分类管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、垃圾列表 2、公告信息管理 3、公告类型管理 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 …

【王道数据结构】【chapter5树与二叉树】【P159t15】

设计一个算法将二叉树的叶结点从左到右的顺序连成一个单链表&#xff0c;表头指针为head。二叉树按二叉链表方式存储&#xff0c;链接时用叶结点的右指针来存放单链表指针。 #include <iostream> #include <stack> #include <queue> typedef struct treenode…

【AIGC】Stable Diffusion 的提示词入门

一、正向提示词和反向提示词 Stable Diffusion 中的提示词通常用于指导用户对生成的图像进行控制。这些提示词可以分为正向提示词&#xff08;Positive Prompts&#xff09;和反向提示词&#xff08;Negative Prompts&#xff09;两类&#xff0c;它们分别影响图像生成过程中的…

《春山》中的贝叶斯统计——白敬亭衣服合理概率及决策比重。

目录 1. 全身黑衣服合理概率2. 真的是导演组允许&#xff1f;3. 粉丝的证据是否站得住&#xff1f;4.总结 感谢up主链接: 【理工春山学】只谈事实 从统计角度深度剖析春山学&#xff0c;她使用贝叶斯统计合理分析了在舞台中白敬亭、双魏、导演组出错的概率。接下来我采用一个新…

继承树追溯

属性/方法查找顺序&#xff1a;&#xff08;比如&#xff1a;查找变量h&#xff09; 查找当前类中有没有属性h 依次上溯每个父类&#xff0c;查看每个父类中是否有h&#xff0c;直到Object 如果没找到&#xff0c;则出现编译错误 上面步骤&#xff0c;只要找到h变量&#xff0c…

吐血整理!操作系统【处理机调度】

&#x1f308;个人主页&#xff1a;godspeed_lucip &#x1f525; 系列专栏&#xff1a;OS从基础到进阶 1 基本概念1.1 总览1.2 什么是调度1.2.1 调度1.2.2 处理机调度 1.3 调度的三个层次1.3.1 高级调度1.3.2 中级调度&#xff08;内存调度&#xff09;1.3.3 低级调度&#xf…

大模型爆款应用fabric_构建优雅的提示

项目地址&#xff1a;https://github.com/danielmiessler/fabric 1 引言 目前 fabric 已经获得了 5.3K Star&#xff0c;其中上周获得了 4.2K&#xff0c;成为了上周热榜的第二名&#xff08;第一名是免费手机看电视的 Android 工具&#xff09;&#xff0c;可以算是爆款应用…

v-model的原理

v-model是v-bind和v-on结合在一起的语法糖&#xff0c;是用于原生标签&#xff08;如input、textarea…&#xff09;和自定义组件上的一个简化包装。它本身并不会做出什么&#xff0c;只会将表单元素的值&#xff08;input/textarea等&#xff09;与数据对象进行双向绑定。v-mo…

【leetcode994】腐烂的橘子(BFS)

文章目录 一、题目二、思路三、代码 一、题目 二、思路 首先将所有烂橘子入队&#xff0c;然后常规BFS遍历&#xff0c;注意while的截止条件除了队列为空&#xff0c;新鲜橘子数量大于0&#xff08;没新鲜橘子也没必要继续遍历&#xff0c;保证时间计算的正确性&#xff09;&a…

哈希切分

目录 一 二 三 2.单个子文件太大怎么办&#xff1f;&#xff08;分两种情况讨论&#xff09; 一 这样的题目典型就是KV模型的问题&#xff0c;即通过key IP找对应的value 出现次数&#xff0c;对于KV模型的问题首先想到的就是用map来统计次数&#xff0c;但是100G大小的文件…

Docker 第十四章 : Docker 三剑客之 Machine

第十四章 : Docker 三剑客之 Machine 本章知识点: Docker Machine 是 Docker 三剑客之一,它是一个工具,允许用户在本地或远程机器上创建 Docker 主机。它简化了 Docker 环境的设置,特别是在不同的操作系统和云平台上。通过 Docker Machine,用户可以轻松地在虚拟机或物理…

基于四叉树的图像分割算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ........................................................... Imgs(dx 1 : dx R1, dy 1 …

Spring中的事务实现、失效场景即AOP的相关概念理解

spring实现事务&#xff08;声明式事务&#xff09;的本质就是aop完成的,它会对方法的前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或回滚事务。aop就是面向切面编程&#xff0c;在spring中将那些与业务无关&#xff0c;但却对多个对象产生影响的…

【硬核】javascript轻松实现自动化批量取消某音用户关注功能

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;web开发者、设计师、技术分享博主 &#x1f40b; 希望大家多多支持一下, 我们一起学习和进步&#xff01;&#x1f604; &#x1f3c5; 如果文章对你有帮助的话&#xff0c;欢迎评论 &#x1f4ac;点赞&a…

SQL世界之函数+语句(九,十)

目录 一、SQL 函数 1.函数的语法 2.函数的类型 1.合计函数&#xff08;Aggregate functions&#xff09; 2.Scalar 函数 二、SQL AVG 函数 1.AVG 函数 2.SQL AVG() 语法 3.SQL AVG() 实例 三、SQL COUNT() 函数 1.COUNT() 函数 2.SQL COUNT() 语法 3.SQL COUNT(co…

高程 | 继承与派生(c++)

文章目录 &#x1f4da;继承的概念和语法&#x1f4da;派生类生成过程&#x1f4da;继承权限和继承方式&#x1f407;公有继承&#x1f407;私有继承&#x1f407;保护继承 &#x1f4da;类型转换规则&#x1f4da;派生类构造函数和析构函数&#x1f4da;继承中的静态成员特性&…