vue parseHTML函数解析器遇到结束标签

目录
  • 引言
    • match函数匹配正则endTag
  • 关键 parseEndTag 函数代码
    • 总结parseEndTag 函数作用
    • handleStartTag函数后续
    • 最后更新 stack 栈以及 lastTag

引言

承接上篇 parseHTML 函数源码解析拿到返回值后的处理

接下来我们将会讲解当 textEnd === 0 解析器遇到结束标签,parse 结束标签的代码如下:

// End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
	var curIndex = index;
	advance(endTagMatch[0].length);
	parseEndTag(endTagMatch[1], curIndex, index);
	continue
}

match函数匹配正则endTag

首先调用 html 字符串的 match 函数匹配正则 endTag ,将结果保存在常量endTagMatch中。正则 endTag 用来匹配结束标签,并且拥有一个捕获组用来捕获标签名字,比如有如下html 字符串:

<div></div>

endTagMatch 输出如下:

endTagMatch = [
  '</div>',
  'div'
]

第一个元素是整个匹配到的结束标签字符串

第二个元素是对应的标签名字。

如果匹配成功 if 语句块的代码将被执行,首先使用 curIndex 常量存储当前 index 的值,然后调用 advance 函数,并以 endTagMatch[0].length 作为参数,接着调用了 parseEndTag 函数对结束标签进行解析,传递给 parseEndTag 函数的三个参数分别是:标签名以及结束标签在 html 字符串中起始和结束的位置,最后调用 continue 语句结束此次循环。

关键 parseEndTag 函数代码

现在我们来讲解下关键 parseEndTag 函数代码如下:

function parseEndTag(tagName, start, end) {
	var pos, lowerCasedTagName;
	if (start == null) {
		start = index;
	}
	if (end == null) {
		end = index;
	}
	// Find the closest opened tag of the same type
	if (tagName) {
		lowerCasedTagName = tagName.toLowerCase();
		for (pos = stack.length - 1; pos >= 0; pos--) {
			if (stack[pos].lowerCasedTag === lowerCasedTagName) {
				break
			}
		}
	} else {
		// If no tag name is provided, clean shop
		pos = 0;
	}
	if (pos >= 0) {
		// Close all the open elements, up the stack
		for (var i = stack.length - 1; i >= pos; i--) {
			if (i > pos || !tagName &&
				options.warn
			) {
				options.warn(
					("tag <" + (stack[i].tag) + "> has no matching end tag.")
				);
			}
			if (options.end) {
				options.end(stack[i].tag, start, end);
			}
		}
		// Remove the open elements from the stack
		stack.length = pos;
		lastTag = pos && stack[pos - 1].tag;
	} else if (lowerCasedTagName === 'br') {
		if (options.start) {
			options.start(tagName, [], true, start, end);
		}
	} else if (lowerCasedTagName === 'p') {
		if (options.start) {
			options.start(tagName, [], false, start, end);
		}
		if (options.end) {
			options.end(tagName, start, end);
		}
	}
}

你需要知道 parseEndTag 函数调用之前已经获得到了结束标签的名字以及结束标签在html(template)字符串中的起始和结束位置。 但是这并不代表着 html parser 结束了。

为什么?

还记得我们之前讲的 stack 栈吗? 之前我们讲到通过stack可以检测是否有非一元标签是否微写闭合标签,接下来还会处理 stack 栈中剩余的标签。

除了这些功能之外,parseEndTag函数还会做一件事儿,如果你感兴趣你可以在任何html文件中写下如下内容:

<body>
  </br>
  </p>
</body>

上面的html片段中,我们分别写了</br>、</p>的结束标签,但注意我们并没有写起始标签,然后浏览器是能够正常解析他们的,其中 </br> 标签被正常解析为 <br> 标签,而</p>标签被正常解析为 <p></p> 。除了 br 与 p 其他任何标签如果你只写了结束标签那么浏览器都将会忽略。所以为了与浏览器的行为相同,parseEndTag 函数也需要专门处理br与p的结束标签,即:</br> 和</p>。

总结parseEndTag 函数作用

  • 检测是否缺少闭合标签
  • 处理 stack 栈中剩余的标签
  • 解析</br> 与标签,与浏览器的行为相同

当一个函数拥有两个及以上功能的时候,最常用的技巧就是通过参数进行控制,还记得jQuery中的Access 吗?parseEndTag 函数接收三个参数,这三个参数其实都是可选的,根据传参的不同其功能也不同。

  • 第一种是处理普通的结束标签,此时三个参数都传递
  • 第二种是只传递第一个参数
  • 第三种是不传递参数,处理 stack 栈剩余未处理的标签。

代码并不复杂我们一起来看下吧!

var pos, lowerCasedTagName;
if (start == null) {
	start = index;
}
if (end == null) {
	end = index;
}

定了两个变量:pos和 lowerCasedTagName,其中变量 pos 会在后面用于判断 html 字符串是否缺少结束标签,lowerCasedTagName 变量用来存储 tagName 的小写版。

接着是两句if 语句,当 start 和 end 不存在时,将这两个变量的值设置为当前字符流的读入位置,即index。

所以当我们看到这两个 if 语句时,我们就应该能够想到:parseEndTag 函数的第二个参数和第三个参数都是可选的。

其实这种使用 parseEndTag 函数的方式我们在handleStartTag 函数中见过,当时我们没有对其进行讲解一起来回顾下。

if (expectHTML) {
  if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
    parseEndTag(lastTag)
  }
  if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
    parseEndTag(tagName)
  }
}

我们知道 lastTag 引用的是stack栈顶的元素,也就是最近(或者说上一次)遇到的开始标签,所以如下判断条件:

lastTag === 'p' && isNonPhrasingTag(tagName)

这里想表达的意思是:最近一次遇到的开始标签是 p 标签,并且当前正在解析的开始标签必须不能是段落式内容(Phrasing content)模型,这时候 if 语句块的代码才会执行,即调用parseEndTag(lastTag)。

首先大家要知道每一个 html 元素都拥有一个或多个内容模型(content model),其中p 标签本身的内容模型是流式内容(Flow content),并且 p 标签的特性是只允许包含段落式内容(Phrasing content)。

所以条件成立的情况如下:

<p><h1></h1></p>

在解析上面这段 html 字符串的时候,首先遇到p标签的开始标签,此时lastTag被设置为 p ,紧接着会遇到 h1 标签的开始标签,由于 h2 标签的内容模型属于非段落式内容(Phrasing content)模型,所以会立即调用 parseEndTag(lastTag) 函数闭合 p 标签,此时由于强行插入了</p> 标签,所以解析后的字符串将变为如下内容:

<p></p><h2></h2></p>

接着,继续解析该字符串,会遇到 <h2></h2> 标签并正常解析之,最后解析器会遇到一个单独的p 标签的结束标签,即:</p>。

这个时候就回到了我们前面讲过的,当解析器遇到 p 标签或者 br 标签的结束标签时会补全他们,最终<p><h2></h2></p> 这段 html 字符串将被解析为:

<p></p><h2></h2><p></p>

而这也就是浏览器的行为,以上是第一个if 分支的意义。还有第二个if分支,它的条件如下:

canBeLeftOpenTag(tagName) && lastTag === tagName

以上条件成立的意思是:当前正在解析的标签是一个可以省略结束标签的标签,并且与上一次解析到的开始标签相同,如下:

<p>max
<p>kaixin

p 标签是可以省略结束标签的标签,所以当解析到一个p标签的开始标签并且下一次遇到的标签也是p标签的开始标签时,会立即关闭第二个p标签。即调用:parseEndTag(tagName) 函数,然后由于第一个p标签缺少闭合标签所以会Vue会给你一个警告。

handleStartTag函数后续

接下来我们继续讲解handleStartTag函数后续的内容。

if (tagName) {
	lowerCasedTagName = tagName.toLowerCase();
	for (pos = stack.length - 1; pos &gt;= 0; pos--) {
		if (stack[pos].lowerCasedTag === lowerCasedTagName) {
			break
		}
	}
} else {
	// If no tag name is provided, clean shop
	pos = 0;
}

如果tagName存在,lowerCasedTagName 获取的是 tagName 小写之后的值,接下来开启一个 for 循环从后向前遍历 stack 栈,直到找到相应的位置,并且该位置索引会保存到 pos 变量中,如果 tagName 不存在,则直接将 pos 设置为 0 。

开头我们讲到 pos 变量会被用来判断是否有元素缺少闭合标签。怎么做到的呢?看完下面的代码你就明白了。

if (pos >= 0) {
	// Close all the open elements, up the stack
	for (var i = stack.length - 1; i >= pos; i--) {
		if (i > pos || !tagName &&
			options.warn
		) {
			options.warn(
				("tag <" + (stack[i].tag) + "> has no matching end tag.")
			);
		}
		if (options.end) {
			options.end(stack[i].tag, start, end);
		}
	}
	// Remove the open elements from the stack
	stack.length = pos;
	lastTag = pos && stack[pos - 1].tag;
} else if (lowerCasedTagName === 'br') {
	if (options.start) {
		options.start(tagName, [], true, start, end);
	}
} else if (lowerCasedTagName === 'p') {
	if (options.start) {
		options.start(tagName, [], false, start, end);
	}
	if (options.end) {
		options.end(tagName, start, end);
	}
}

上面代码由三部分组成,即if...else if...else if。首先我们查看 if 语句块,当 pos >= 0 的时候就会走 if 语句块。在 if 语句块内开启一个 for 循环,同样是从后向前遍历 stack 数组,如果发现 stack 数组中存在索引大于 pos 的元素,那么该元素一定是缺少闭合标签的,这个时候如果是在非生产环境那么 Vue 便会打印一句警告,告诉你缺少闭合标签。除了打印一句警告之外,随后会调用 options.end(stack[i].tag, start, end) 立即将其闭合,这是为了保证解析结果的正确性。

最后更新 stack 栈以及 lastTag

stack.length = pos;
lastTag = pos && stack[pos - 1].tag;

了解下剩下的两个else if:

if (pos >= 0) {
  // ... 省略
} else if (lowerCasedTagName === 'br') {
  if (options.start) {
    options.start(tagName, [], true, start, end)
  }
} else if (lowerCasedTagName === 'p') {
  if (options.start) {
    options.start(tagName, [], false, start, end)
  }
  if (options.end) {
    options.end(tagName, start, end)
  }
}

这两个else if 什么情况下成立呢?

  • 当 tagName 没有在 stack 栈中找到对应的开始标签时,pos 为 -1 。
  • tagName为br 、p标签。

当你写了 br 标签的结束标签:</br> 或 p 标签的结束标签 </p> 时,解析器能够正常解析他们,其中对于 </br> 会将其解析为正常的 <br> 标签,而 </p> 标签也会正常解析为<p></p>。

可以发现对于 </br> 和 </p> 标签浏览器可以将其正常解析为 <br> 以及<p></p>,Vue 的 parser 与浏览器的行为是一致的。

现在我们还剩一个问题没有讲解,即parseEndTag是如何处理stack栈中剩余未处理的标签的。其实就是调用 parseEndTag() 函数时不传递任何参数,也就是说此时 tagName 参数也不存在。这个时候我们再次查看下面的代码:

由于 pos 为 0 ,所以 i >= pos 始终成立,这个时候 stack 栈中如果有剩余未处理的标签,则会逐个警告缺少闭合标签,并调用 options.end 将其闭合。

以上对于整个词法分析的过程重点部分就已经讲解完毕了,其实现方式就是通过读取字符流配合正则一点一点的解析字符串,直到整个字符串都被解析完毕为止。并且每当遇到一个特定的token 时都会调用相应的钩子函数,同时将有用的参数传递过去。比如每当遇到一个开始标签都会调用 options.start 钩子函数,遇到闭合标签调用 options.end 钩子函数。

下面我们来讲讲这两个重要的钩子函数,并且谈下AST的基本形成。

parseHTML 函数源码解析 AST 基本形成

以上就是vue parseHTML函数解析器遇到结束标签的详细内容,更多关于vue parseHTML函数的资料请关注我们其它相关文章!

(0)

相关推荐

  • vue parseHTML 函数拿到返回值后的处理源码解析

    目录 引言 parseStartTag函数返回值 handleStartTag源码 tagName 及unarySlash 调用parser钩子函数 引言 继上篇文章: parseHTML 函数源码解析 var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html))

  • vue parseHTML 函数源码解析AST基本形成

    目录 AST(抽象语法树)? 子节点 Vue中是如何把html(template)字符串编译解析成AST 解析html 代码重新改造 接着解析 html (template)字符串 解析div AST(抽象语法树)? 在上篇文章中我们已经把整个词法分析的解析过程分析完毕了. 例如有html(template)字符串: <div id="app"> <p>{{ message }}</p> </div> 产出如下: { attrs: [&q

  • vue parseHTML函数源码解析start钩子函数

    目录 正文 platformGetTagNamespace 源码 isForbiddenTag 函数 addIfCondition是什么 processIfConditions 源码 正文 接上章节:parseHTML 函数源码解析 AST 预备知识 现在我们就可以愉快的进入到Vue start钩子函数源码部分了. start: function start(tag, attrs, unary) { // check namespace. // inherit parent ns if ther

  • vue parseHTML源码解析hars end comment钩子函数

    目录 引言 chars源码: parseText end 源码: 引言 接上文  parseHTML 函数源码解析(六) start钩子函数 接下来我们主要讲解当解析器遇到一个文本节点时会如何为文本节点创建元素描述对象,又会如何对文本节点做哪些特殊的处理. parseHTML(template, { chars: function(){ //... }, //... }) chars源码: chars: function chars(text) { if (!currentParent) { {

  • vue parseHTML函数源码解析 AST预备知识

    目录 正文 createASTElement函数 解析指令所用正则 parse 函数中的变量 正文 接上章节:parseHTML 函数源码解析AST 基本形成 在正式扎进Vue parse源码之前,我们先了解下他周边的工具函数, 这能帮我们快速的去理解阅读. 还记得我们在上章节讲的element元素节点的描述对象吗? var element = { type: 1, tag: tag, parent: null, attrsList: attrs, children: [] } 在源码中定义了一

  • vue parseHTML 函数源码解析

    目录 正文 函数开头定义的一些常量和变量 while 循环 textEnd ===0 parseStartTag 函数解析开始标签 总结: 正文 接上篇: Vue编译器源码分析AST 抽象语法树 function parseHTML(html, options) { var stack = []; var expectHTML = options.expectHTML; var isUnaryTag$$1 = options.isUnaryTag || no; var canBeLeftOpen

  • vue parseHTML函数解析器遇到结束标签

    目录 引言 match函数匹配正则endTag 关键 parseEndTag 函数代码 总结parseEndTag 函数作用 handleStartTag函数后续 最后更新 stack 栈以及 lastTag 引言 承接上篇 parseHTML 函数源码解析拿到返回值后的处理 接下来我们将会讲解当 textEnd === 0 解析器遇到结束标签,parse 结束标签的代码如下: // End tag: var endTagMatch = html.match(endTag); if (endTa

  • Vue 2源码解析ParseHTML函数HTML模板

    ParseHTML函数 - HTML 模板解析 之前在解析 parse 函数时,我们知道整个 解析 template 模板并生成 ast 对象 的过程都发生在这个函数的执行过程中. 但是 parse 函数内部本身只定义了一些标签.指令的处理方法和警告函数,并且在传递给 parseHTML 函数的参数中定义了四个处理方法. 最终是通过调用 parseHTML 来解析 template 模板 整个解析过程,其实就是 通过一系列正则表达式来匹配 template 模板字符串,并截取该部分匹配内容并重新

  • 从零实现一个vue文件解析器

    如何从 0 处理一个 vue 文件并实现简单的响应式? 在现在的前端工程化中,打包工具是不可或缺的,其中webpack无疑是占据了主导地位,当然也有尤大搞的vite,但是论生态和使用人数,至少在目前webpack还是更胜一筹. 打包工具能帮助我们打包前端文件,在webpack中,不同后缀的文件通过不同loader来处理. 本文就讨论下怎么实现一个处理.vue文件的loader,以及用loader处理完.vue文件怎么把内容渲染在浏览器上,并实现简单的响应式. 源码地址 gezhicui/vue-

  • Vue源码解析之Template转化为AST的实现方法

    什么是AST 在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式. Virtual Dom Vue的一个厉害之处就是利用Virtual DOM模拟DOM对象树来优化DOM操作的一种技术或思路. Vue源码中虚拟DOM构建经历 template编译成AST语法树 -> 再转换为render函数 最终返回一个VNode(VNod

随机推荐