vue parseHTML源码解析hars end comment钩子函数
作者:李李 发布时间:2024-06-05 15:29:14
引言
接上文 parseHTML 函数源码解析 start钩子函数
接下来我们主要讲解当解析器遇到一个文本节点时会如何为文本节点创建元素描述对象,又会如何对文本节点做哪些特殊的处理。
parseHTML(template, {
chars: function(){
//...
},
//...
})
chars源码:
chars: function chars(text) {
if (!currentParent) {
{
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.'
);
} else if ((text = text.trim())) {
warnOnce(
("text \"" + text + "\" outside root element will be ignored.")
);
}
}
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
var children = currentParent.children;
text = inPre || text.trim() ?
isTextTag(currentParent) ? text : decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
:
preserveWhitespace && children.length ? ' ' : '';
if (text) {
var res;
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text: text
});
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
children.push({
type: 3,
text: text
});
}
}
}
当解析器遇到文本节点时,如上代码中的 chars 钩子函数就会被调用,并且接收该文本节点的文本内容作为参数。
我们来看chars钩子函数最开始的这段代码:
if (!currentParent) {
{
if (text === template) {
warnOnce(
'Component template requires a root element, rather than just text.'
);
} else if ((text = text.trim())) {
warnOnce(
("text \"" + text + "\" outside root element will be ignored.")
);
}
}
return
}
首先判断了 currentParent 变量是否存在,我们知道 currentParent 变量指向的是当前节点的父节点:。
如果 currentParent 变量不存在说明什么问题?
1:没有根元素,只有文本。
2: 文本在根元素之外。
当遇到第一种情况打印警告信息:"模板必须要有根元素",第二种情况打印警告信息:" 根元素外的文本将会被忽略"。
接下来:
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
这段代码是用来解决 IE 浏览器中渲染 <textarea> 标签的 placeholder 属性时存在的 bug 的。具体的问题大家可以在这个 issue 查看。
接下来是个嵌套三元表达式:
var children = currentParent.children;
text = inPre || text.trim() ?
isTextTag(currentParent) ? text : decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
:
preserveWhitespace && children.length ? ' ' : '';
这个嵌套三元表达式判断了条件 inPre || text.trim() 的真假,如果为 true,检测了当前文本节点的父节点是否是文本标签,如果是文本标签则直接使用原始文本,否则使用decodeHTMLCached 函数对文本进行解码。
inPre || text.trim() 如果为 false,检测 preserveWhitespace 是否为 true 。preserveWhitespace 是一个布尔值代表着是否保留空格,只有它为真的情况下才会保留空格。但即使 preserveWhitespace 常量的值为真,如果当前节点的父节点没有子元素则也不会保留空格,换句话说,编译器只会保留那些 不存在于开始标签之后的空格。
接下来:
if (text) {
var res;
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text: text
});
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
children.push({
type: 3,
text: text
});
}
}
这里相当就比较简单了一个 if else if 操作,第一个 if 判断当前元素未使用v-pre 指令,text不为空,使用 parseText 函数成功解析当前文本节点的内容。
对于前两个条件很好理解,关键在于 parseText 函数能够成功解析文本节点的内容说明了什么,如下示例代码:
<div> hello: {{ message }} </div>
如上模板中存在的文本节点包含了 Vue 语法中的字面量表达式,而 parseText 函数的作用就是用来解析这段包含了字面量表达式的文本的。此时会执行以下代码创建一个类型为2(type = 2) 的元素描述对象:
children.push({
type: 2,
expression: res.expression,
tokens: res.tokens,
text: text
});
注意:类型为 2 的元素描述对象拥有三个特殊的属性,分别是 expression 、tokens 以及text ,其中 text 就是原始的文本内容,而 expression 和 tokens 的值是通过 parseText 函数解析的结果中读取的。
后面我们专门会讲讲parseText函数,接下来继续看下如果上列的 if 判断失败出现的三种可能性。
当前解析的元素使用v-pre 指令
text 为空
parseText 解析失败
只要以上三种情况中,有一种情况出现则代码会来到else...if 分支的判断,如下:
else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
children.push({
type: 3,
text: text
});
}
如果满足 else if 中的条件直接,创建一个类型为3(type = 3) 的元素描述对象:类型为3 的元素描述对象只拥有一个的属性text存储原始的文本内容。
在看下要满足 else if 中的这些条件吧!
文本内容不是空格
文本内容是空格,但是该文本节点的父节点还没有子节点(即 !children.length )
文本内容是空格,并且该文本节点的父节点有子节点,但最后一个子节点不是空格
接下来我们来聊聊之前讲到的parseText 函数。
parseText
function parseText(
text,
delimiters
) {
var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
if (!tagRE.test(text)) {
return
}
var tokens = [];
var rawTokens = [];
var lastIndex = tagRE.lastIndex = 0;
var match, index, tokenValue;
while ((match = tagRE.exec(text))) {
index = match.index;
// push text token
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index));
tokens.push(JSON.stringify(tokenValue));
}
// tag token
var exp = parseFilters(match[1].trim());
tokens.push(("_s(" + exp + ")"));
rawTokens.push({
'@binding': exp
});
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex));
tokens.push(JSON.stringify(tokenValue));
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
parseText 接收两个参数 text 要解析的文本,delimiters 是编译器的一个用户自定义选项delimiters ,通过它可以改变文本插入分隔符。所以才有了如下代码。
var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
这里是解析文本所用正则之间的一个较量,delimiters 有值就调用buildRegex函数,我们默认是没有值,使用 defaultTagRE 来解析文本。
var defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
这个正则还是非常简单,接下来会判断,如果文本中没有与正则相匹配的文本直接直接终止函数的执行。
if (!tagRE.test(text)) {
return
}
接下来代码就有意思了一起看下。
var tokens = [];
var rawTokens = [];
var lastIndex = tagRE.lastIndex = 0;
var match, index, tokenValue;
while ((match = tagRE.exec(text))) {
index = match.index;
// push text token
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index));
tokens.push(JSON.stringify(tokenValue));
}
// tag token
var exp = parseFilters(match[1].trim());
tokens.push(("_s(" + exp + ")"));
rawTokens.push({
'@binding': exp
});
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex));
tokens.push(JSON.stringify(tokenValue));
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
这段代码不难,初始定义了一系列变量。 接着开启一个while循环,使用 tagRE 正则匹配文本内容,并将匹配结果保存在 match 变量中,直到匹配失败循环才会终止,这时意味着所有的字面量表达式都已经处理完毕了。
在这个while循环结束返回一个对象,expression、tokens分别存储解析过程中的信息。
假设文本如下:
<div id="app">hello {{ message }}</div>
parseText 解析文本后返回的对象。
{
expression: "'hello'+_s(message)",
tokens: [
'hello',
{
'@binding': 'message'
}
]
}
接下来我们聊聊对结束标签的处理。
end 源码
end: function end() {
// remove trailing whitespace
var element = stack[stack.length - 1];
var lastNode = element.children[element.children.length - 1];
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
element.children.pop();
}
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
closeElement(element);
}
end 钩子函数,当解析 html 字符串遇到结束标签的时候,。那么在 end 钩子函数中都需要做哪些事情呢?
在之前的文章中我们讲过解析器遇到非一元标签的开始标签时,会将该标签的元素描述对象设置给 currentParent 变量,代表后续解析过程中遇到的所有标签都应该是 currentParent 变量所代表的标签的子节点,同时还会将该标签的元素描述对象添加到 stack 栈中。
而当遇到结束标签的时候则意味着 currentParent 变量所代表的标签以及其子节点全部解析完毕了,此时我们应该把 currentParent 变量的引用修改为当前标签的父标签,这样我们就将作用域还原给了上层节点,以保证解析过程中正确的父子关系。
下面代码就是来完成这个工作:
stack.length -= 1;
currentParent = stack[stack.length - 1];
closeElement(element);
首先将当前节点出栈:stack.length -= 1 什么意思呢?
看一个代码就懂了。
var arr = [1,2,3,4];
arr.length-=1;
>arr [1,2,3]
接着读取出栈后 stack 栈中的最后一个元素作为 currentParent 变量的值。 那closeElement 函数是做什么用的呢?
closeElement 源码
function closeElement(element) {
// check pre state
if (element.pre) {
inVPre = false;
}
if (platformIsPreTag(element.tag)) {
inPre = false;
}
// apply post-transforms
for (var i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options);
}
}
closeElement 的作用有两个:第一个是对数据状态的还原,第二个是调用后置处理转换钩子函数。
接下来看下end函数中剩余代码:
// remove trailing whitespace
var element = stack[stack.length - 1];
var lastNode = element.children[element.children.length - 1];
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
element.children.pop();
}
这个代码的作用是去除当前元素最后一个空白子节点,我们在讲解 chars 钩子函数时了解到:preserveWhitespace 只会保留那些不在开始标签之后的空格,所以当空白作为标签的最后一个子节点存在时,也会被保留,如下代码所示:
<div><span>test</span> <!-- 空白占位 --> </div>
如上代码中 <span> 标签的结束标签与 <div> 标签的结束标签之间存在一段空白,这段空白将会被保留。如果这段空白被保留那么就可能对布局产生影响,尤其是对行内元素的影响。
为了消除这些影响带来的问题,好的做法是将它们去掉,而如代码就是用来完成这个工作的。
comment 注释节点描述对象
解析器是否会解析并保留注释节点,是由 shouldKeepComment 编译器选项决定的,开发者可以在创建Vue 实例的时候通过设置 comments 选项的值来控制编译器的shouldKeepComment 选项。默认情况下 comments 选项的值为 false ,即不保留注释,假如将其设置为 true ,则当解析器遇到注释节点时会保留该注释节点,此时 parseHTML 函数的 comment 钩子函数会被调用,如下:
comment: function comment(text) {
currentParent.children.push({
type: 3,
text: text,
isComment: true
});
}
要注意的是,普通文本节点与注释节点的元素描述对象的类型是一样的都是 3 ,不同的是注释节点的元素描述对象拥有 isComment 属性,并且该属性的值为 true,目的就是用来与普通文本节点作区分的。
来源:https://zhuanlan.zhihu.com/p/92659958


猜你喜欢
- 本文研究的主要是Python内建模块struct的相关内容,具体如下。Python中变量的类型只有列表、元祖、字典、集合等高级抽象类型,并没
- sql查询数组中是否包含某个内容find_in_set如果表Atable中的a字段内容是1,2,3 的格式(a字段是用,分隔的),要查询该字
- 您是否知道 OpenCV 具有执行行人检测的内置方法?OpenCV 附带一个预训练的 HOG + 线性 SVM 模型,可用于在图像和视频流中
- asp fso type属性取得文件类型代码是用来返回类型指定的文件或文件夹。语法FileObject.Type FolderObject.
- 如何做一个专门显示文本文件的页面? 代码如下:txt.asp<html><head&g
- linux安装mysql服务分两种安装方法:①源码安装,优点是安装包比较小,只有十多M,缺点是安装依赖的库多,安装编译时间长,安装步骤复杂容
- 一. Python中表示时间的两种方式:时间戳:相对于1970.1.1 00:00:00以秒计算的偏移量,唯一的时间元组struct_tim
- 集群是一种实现高可用性的有效解决方案,有时它会适得其反。而且,它还非常昂贵。因此,数据库管理员可使用日志转移代替集群来提供较高的可用性。日志
- 前 言:作为当前先进的深度学习目标检测算法YOLOv5,已经集合了大量的trick,但是在处理一些复杂背景问题的时候,还是容易出现错漏检的问
- 代码示例#输入'''order_id:31489join_course[0][join_tel]:131309998
- VueConf ,尤大说, Vue 支持 Ts 了,网上关于 Vue + Ts 的资料有点少, 楼主踩了一个星期坑,终于摸明白了 修饰器 的
- 前言:本文的主要内容是介绍Python中的列表及其方法的使用,涉及到的方法包括对列表元素进行修改、添加、删除、排序以及求列表长度等,此外还介
- 如下所示:logging: config: classpath:spring-logback.xml pattern: console: &
- 1.添加文件touch /Library/LaunchDaemons/com.mysql.mysql.plist2.添加内容<?xml
- 作为一个信号库,使用时候是支持一对一以及一对多的订阅模式,可以实现发送数据等,一般情况下,只要能够使用到Blinker的,一般都是应用在技术
- 这是学习tensorflow框架中遇到的知识,这里定义函数的时候选用的是将x和y封装起来,方便tensorflow求导。要慢慢习惯这种写法i
- 这个类可以用来搜索在给定的文本目录中的文件。 它可以给定目录遍历递归查找某些文件扩展名的文件。 并打开找到的文件,并检查他们是否包含搜索词语
- 一个简单的验证码爬取程序本文介绍了在Python2.7环境下爬取网站验证码:思路就是获取验证码对应的url,然后发起requst请求,读取该
- 虽然有很多种方式可以解决这个问题,但是我们可以用T-SQL代码来处理这个文件删除过程。我用xp_cmdshell命令和FORFILES命令来
- 一、概念 1. 数据库 (Database)什么是数据库?数据库是依照某种数据模型组织起来并存放二级存储器中的数据集合。这种数据集合具有如下