通过前面的例子,我们发现一个很有趣的事,那就是每一轮截取字符串时,都是在整个模板的开始位置截取。我们根据模板开始位置的片段类型,进行不同的截取操作。
例如,上面例子中的第一轮循环:如果是以开始标签开头的模板,就把开始标签截取掉。
再例如,上面例子中的第四轮循环:如果是以文本开始的模板,就把文本截取掉。
这些被截取的片段分很多种类型,示例如下。
-
开始标签,例如
<div>
。 -
结束标签,例如
</div>
。 -
HTML注释,例如
<!-- 我是注释 -->
。 -
DOCTYPE,例如
<!DOCTYPE html>
。 -
条件注释,例如
<!--[if !IE]>-->我是注释<!--<![endif]-->
。 -
文本,例如
我是Berwin
。 -
通常,最常见的是开始标签、结束标签、文本以及注释。
2 截取开始标签
上一节中我们说过,每一轮循环都是从模板的最前面截取,所以只有模板以开始标签开头,才需要进行开始标签的截取操作。
那么,如何确定模板是不是以开始标签开头?
在HTML解析器中,想分辨出模板是否以开始标签开头并不难,我们需要先判断HTML模板是不是以<开头。
如果HTML模板的第一个字符不是<,那么它一定不是以开始标签开头的模板,所以不需要进行开始标签的截取操作。
如果HTML模板以<开头,那么说明它至少是一个以标签开头的模板,但这个标签到底是什么类型的标签,还需要进一步确认。
如果模板以<开头,那么它有可能是以开始标签开头的模板,同时它也有可能是以结束标签开头的模板,还有可能是注释等其他标签,因为这些类型的片段都以<开头。那么,要进一步确定模板是不是以开始标签开头,还需要借助正则表达式来分辨模板的开始位置是否符合开始标签的特征。
那么,如何使用正则表达式来匹配模板以开始标签开头?我们看下面的代码:
const ncname = ‘[a-zA-Z_][\w\-\.]*’
const qnameCapture = ((?:${ncname}\\:)?${ncname})
const startTagOpen = new RegExp(^<${qnameCapture}
)
// 以开始标签开始的模板
‘
’.match(startTagOpen) // [“<div”, “div”, index: 0, input: “ ”]// 以结束标签开始的模板
‘
// 以文本开始的模板
‘我是Berwin
’.match(startTagOpen) // null通过上面的例子可以看到,只有'<div></div>'
可以成功匹配,而以</div>
开头的或者以文本开头的模板都无法成功匹配。
我们介绍了当HTML解析器解析到标签开始时,会触发钩子函数start,同时会给出三个参数,分别是标签名(tagName)、属性(attrs)以及自闭合标识(unary)。
因此,在分辨出模板以开始标签开始之后,需要将标签名、属性以及自闭合标识解析出来。
在分辨模板是否以开始标签开始时,就可以得到标签名,而属性和自闭合标识则需要进一步解析。
当完成上面的解析后,我们可以得到这样一个数据结构:
const start = ‘
’.match(startTagOpen)if (start) {
const match = {
tagName: start[1],
attrs: []
}
}
这里有一个细节很重要:在前面的例子中,我们匹配到的开始标签并不全。例如:
const ncname = ‘[a-zA-Z_][\w\-\.]*’
const qnameCapture = ((?:${ncname}\\:)?${ncname})
const startTagOpen = new RegExp(^<${qnameCapture}
)
‘
’.match(startTagOpen)// [“<div”, “div”, index: 0, input: “
”]‘
’.match(startTagOpen)// [“<p”, “p”, index: 0, input: “
”]‘
’.match(startTagOpen)// [“<div”, “div”, index: 0, input: “
”]可以看出,上面这个正则表达式虽然可以分辨出模板是否以开始标签开头,但是它的匹配规则并不是匹配整个开始标签,而是开始标签的一小部分。
事实上,开始标签被拆分成三个小部分,分别是标签名、属性和结尾,如图3所示。
图3 开始标签被拆分成三个小部分(代码用代码体)
通过“标签名”这一段字符,就可以分辨出模板是否以开始标签开头,此后要想得到属性和自闭合标识,则需要进一步解析。
1. 解析标签属性
在分辨模板是否以开始标签开头时,会将开始标签中的标签名这一小部分截取掉,因此在解析标签属性时,我们得到的模板是下面伪代码中的样子:
’ class=“box”>’
通常,标签属性是可选的,一个标签的属性有可能存在,也有可能不存在,所以需要判断标签是否存在属性,如果存在,对它进行截取。
下面的伪代码展示了如何解析开始标签中的属性,但是它只能解析一个属性:
const attribute = /\s*([\s"‘<>/=]+)(?:\s*(=)\s*(?:"([“]*)”+|'([’]*)‘+|([^\s"’=<>`]+)))?/
let html = ’ class=“box”>’
let attr = html.match(attribute)
html = html.substring(attr[0].length)
console.log(attr)
// [’ class=“box”‘, ‘class’, ‘=’, ‘box’, undefined, undefined, index: 0, input: ’ class=“box”>’]
如果标签上有很多属性,那么上面的处理方式就不足以支撑解析任务的正常运行。例如下面的代码:
const attribute = /\s*([\s"‘<>/=]+)(?:\s*(=)\s*(?:"([“]*)”+|'([’]*)‘+|([^\s"’=<>`]+)))?/
let html = ’ class=“box” id=“el”>’
let attr = html.match(attribute)
html = html.substring(attr[0].length)
console.log(attr)
// [’ class=“box”‘, ‘class’, ‘=’, ‘box’, undefined, undefined, index: 0, input: ’ class=“box” id=“el”>’]
可以看到,这里只解析出了class属性,而id属性没有解析出来。
此时剩余的HTML模板是这样的:
’ id=“el”>’
所以属性也可以分成多个小部分,一小部分一小部分去解析与截取。
解决这个问题时,我们只需要每解析一个属性就截取一个属性。如果截取完后,剩下的HTML模板依然符合标签属性的正则表达式,那么说明还有剩余的属性需要处理,此时就重复执行前面的流程,直到剩余的模板不存在属性,也就是剩余的模板不存在符合正则表达式所预设的规则。
例如:
const startTagClose = /^\s*(/?)>/
const attribute = /\s*([\s"‘<>/=]+)(?:\s*(=)\s*(?:"([“]*)”+|'([’]*)‘+|([^\s"’=<>`]+)))?/
let html = ’ class=“box” id=“el”>’
let end, attr
const match = {tagName: ‘div’, attrs: []}
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
html = html.substring(attr[0].length)
match.attrs.push(attr)
}
上面这段代码的意思是,如果剩余HTML模板不符合开始标签结尾部分的特征,并且符合标签属性的特征,那么进入到循环中进行解析与截取操作。
通过match方法解析出的结果为:
{
tagName: ‘div’,
attrs: [
[’ class=“box”', ‘class’, ‘=’, ‘box’, null, null],
[’ id=“el”‘, ‘id’,’=', ‘el’, null, null]
]
}
可以看到,标签中的两个属性都已经解析好并且保存在了attrs中。
此时剩余模板是下面的样子:
“>”
我们将属性解析后的模板与解析之前的模板进行对比:
// 解析前的模板
’ class=“box” id=“el”>’
// 解析后的模板
‘>’
// 解析前的数据
{
tagName: ‘div’,
attrs: []
}
// 解析后的数据
{
tagName: ‘div’,
attrs: [
[’ class=“box”', ‘class’, ‘=’, ‘box’, null, null],
[’ id=“el”‘, ‘id’,’=', ‘el’, null, null]
]
}
可以看到,标签上的所有属性都已经被成功解析出来,并保存在attrs属性中。
2. 解析自闭合标识
如果我们接着上面的例子继续解析的话,目前剩余的模板是下面这样的:
‘>’
开始标签中结尾部分解析的主要目的是解析出当前这个标签是否是自闭合标签。
举个例子:
这样的div标签就不是自闭合标签,而下面这样的input标签就属于自闭合标签:
自闭合标签是没有子节点的,所以前文中我们提到构建AST层级时,需要维护一个栈,而一个节点是否需要推入到栈中,可以使用这个自闭合标识来判断。
那么,如何解析开始标签中的结尾部分呢?看下面这段代码:
function parseStartTagEnd (html) {
const startTagClose = /^\s*(/?)>/
const end = html.match(startTagClose)
const match = {}
if (end) {
match.unarySlash = end[1]
html = html.substring(end[0].length)
return match
}
}
console.log(parseStartTagEnd(‘>’)) // {unarySlash: “”}
console.log(parseStartTagEnd(‘/>
’)) // {unarySlash: “/”}这段代码可以正确解析出开始标签是否是自闭合标签。
从代码中打印出来的结果可以看到,自闭合标签解析后的unarySlash属性为/,而非自闭合标签为空字符串。
3. 实现源码
前面解析开始标签时,我们将其拆解成了三个部分,分别是标签名、属性和结尾。我相信你已经对开始标签的解析有了一个清晰的认识,接下来看一下Vue.js中真实的代码是什么样的:
const ncname = ‘[a-zA-Z_][\w\-\.]*’
const qnameCapture = ((?:${ncname}\\:)?${ncname})
const startTagOpen = new RegExp(^<${qnameCapture}
)
const startTagClose = /^\s*(/?)>/
function advance (n) {
html = html.substring(n)
}
function parseStartTag () {
// 解析标签名,判断模板是否符合开始标签的特征
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: []
}
advance(start[0].length)
// 解析标签属性
let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length)
match.attrs.push(attr)
}
// 判断是否是自闭合标签
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
return match
}
}
}
上面的代码是Vue.js中解析开始标签的源码,这段代码中的html变量是HTML模板。
调用parseStartTag就可以将剩余模板开始部分的开始标签解析出来。如果剩余HTML模板的开始部分不符合开始标签的正则表达式规则,那么调用parseStartTag就会返回undefined。因此,判断剩余模板是否符合开始标签的规则,只需要调用parseStartTag即可。如果调用它后得到了解析结果,那么说明剩余模板的开始部分符合开始标签的规则,此时将解析出来的结果取出来并调用钩子函数start即可:
// 开始标签
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
continue
}
前面我们说过,所有解析操作都运行在循环中,所以continue的意思是这一轮的解析工作已经完成,可以进行下一轮解析工作。
从代码中可以看出,如果调用parseStartTag之后有返回值,那么会进行开始标签的处理,其处理逻辑主要在handleStartTag中。这个函数的主要目的就是将tagName、attrs和unary等数据取出来,然后调用钩子函数将这些数据放到参数中。
3 截取结束标签
结束标签的截取要比开始标签简单得多,因为它不需要解析什么,只需要分辨出当前是否已经截取到结束标签,如果是,那么触发钩子函数就可以了。
那么,如何分辨模板已经截取到结束标签了呢?其道理其实和开始标签的截取相同。
如果HTML模板的第一个字符不是<,那么一定不是结束标签。只有HTML模板的第一个字符是<时,我们才需要进一步确认它到底是不是结束标签。
进一步确认时,我们只需要判断剩余HTML模板的开始位置是否符合正则表达式中定义的规则即可:
const ncname = ‘[a-zA-Z_][\w\-\.]*’
const qnameCapture = ((?:${ncname}\\:)?${ncname})
const endTag = new RegExp(^<\\/${qnameCapture}[^>]*>
)
const endTagMatch = ‘’.match(endTag)
const endTagMatch2 = ‘
console.log(endTagMatch) // [“”, “div”, index: 0, input: “”]
console.log(endTagMatch2) // null
上面代码可以分辨出剩余模板是否是结束标签。当分辨出结束标签后,需要做两件事,一件事是截取模板,另一件事是触发钩子函数。而Vue.js中相关源码被精简后如下:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
html = html.substring(endTagMatch[0].length)
options.end(endTagMatch[1])
continue
}
可以看出,先对模板进行截取,然后触发钩子函数。
4 截取注释
分辨模板是否已经截取到注释的原理与开始标签和结束标签相同,先判断剩余HTML模板的第一个字符是不是<,如果是,再用正则表达式来进一步匹配:
const comment = /^<!–/
if (comment.test(html)) {
const commentEnd = html.indexOf(‘–>’)
if (commentEnd >= 0) {
if (options.shouldKeepComment) {
options.comment(html.substring(4, commentEnd))
}
html = html.substring(commentEnd + 3)
continue
}
}
在上面的代码中,我们使用正则表达式来判断剩余的模板是否符合注释的规则,如果符合,就将这段注释文本截取出来。
这里有一个有意思的地方,那就是注释的钩子函数可以通过选项来配置,只有options.shouldKeepComment为真时,才会触发钩子函数,否则只截取模板,不触发钩子函数。
5 截取条件注释
条件注释不需要触发钩子函数,我们只需要把它截取掉就行了。
截取条件注释的原理与截取注释非常相似,如果模板的第一个字符是<,并且符合我们事先用正则表达式定义好的规则,就说明需要进行条件注释的截取操作。
在下面的代码中,我们通过indexOf找到条件注释结束位置的下标,然后将结束位置前的字符都截取掉:
const conditionalComment = /^<![/
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(‘]>’)
if (conditionalEnd >= 0) {
html = html.substring(conditionalEnd + 2)
continue
}
}
我们来举个例子:
const conditionalComment = /^<![/
let html = ‘<![if !IE]><![endif]>’
if (conditionalComment.test(html)) {
const conditionalEnd = html.indexOf(‘]>’)
if (conditionalEnd >= 0) {
html = html.substring(conditionalEnd + 2)
}
}
console.log(html) // ‘<![endif]>’
从打印结果中可以看到,HTML中的条件注释部分截取掉了。
通过这个逻辑可以发现,在Vue.js中条件注释其实没有用,写了也会被截取掉,通俗一点说就是写了也白写。
6 截取DOCTYPE
DOCTYPE与条件注释相同,都是不需要触发钩子函数的,只需要将匹配到的这一段字符截取掉即可。下面的代码将DOCTYPE这段字符匹配出来后,根据它的length属性来决定要截取多长的字符串:
const doctype = /^]+>/i
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
html = html.substring(doctypeMatch[0].length)
continue
}
示例如下:
const doctype = /^]+>/i
let html = ‘’
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
html = html.substring(doctypeMatch[0].length)
}
console.log(html) // ‘’
从打印结果可以看到,HTML中的DOCTYPE被成功截取掉了。
7 截取文本
若想分辨在本轮循环中HTML模板是否已经截取到文本,其实很简单,我们甚至不需要使用正则表达式。
在前面的其他标签类型中,我们都会判断剩余HTML模板的第一个字符是否是<,如果是,再进一步确认到底是哪种类型。这是因为以<开头的标签类型太多了,如开始标签、结束标签和注释等。然而文本只有一种,如果HTML模板的第一个字符不是<,那么它一定是文本了。
例如:
我是文本
上面这段HTML模板并不是以<开头的,所以可以断定它是以文本开头的。
那么,如何从模板中将文本解析出来呢?我们只需要找到下一个<在什么位置,这之前的所有字符都属于文本,如图4所示。
图4 尖括号前面的字符都属于文本
在代码中可以这样实现:
while (html) {
let text
let textEnd = html.indexOf(‘<’)
// 截取文本
if (textEnd >= 0) {
text = html.substring(0, textEnd)
html = html.substring(textEnd)
}
// 如果模板中找不到<,就说明整个模板都是文本
if (textEnd < 0) {
text = html
html = ‘’
}
// 触发钩子函数
if (options.chars && text) {
options.chars(text)
}
}
上面的代码共有三部分逻辑。
第一部分是截取文本,这在前面介绍过了。<之前的所有字符都是文本,直接使用html.substring从模板的最开始位置截取到<之前的位置,就可以将文本截取出来。
第二部分是一个条件:如果在整个模板中都找不到<,那么说明整个模板全是文本。
第三部分是触发钩子函数并将截取出来的文本放到参数中。
关于文本,还有一个特殊情况需要处理:如果<是文本的一部分,该如何处理?
举个例子:
1<2
在上面这样的模板中,如果只截取第一个<前面的字符,最后被截取出来的将只有1,而不能把所有文本都截取出来。
那么,该如何解决这个问题呢?
有一个思路是,如果将<前面的字符截取完之后,剩余的模板不符合任何需要被解析的片段的类型,就说明这个<是文本的一部分。
什么是需要被解析的片段的类型?我们说过HTML解析器是一段一段截取模板的,而被截取的每一段都符合某种类型,这些类型包括开始标签、结束标签和注释等。
说的再具体一点,那就是上面这段代码中的1被截取完之后,剩余模板是下面的样子:
<2
<2符合开始标签的特征么?不符合。
<2符合结束标签的特征么?不符合。
<2符合注释的特征么?不符合。
当剩余的模板什么都不符合时,就说明<属于文本的一部分。
当判断出<是属于文本的一部分后,我们需要做的事情是找到下一个<并将其前面的文本截取出来加到前面截取了一半的文本后面。
这里还用上面的例子,第二个<之前的字符是<2,那么把<2截取出来后,追加到上一次截取出来的1的后面,此时的结果是:
1<2
截取后剩余的模板是:
如果剩余的模板依然不符合任何被解析的类型,那么重复此过程。直到所有文本都解析完。
说完了思路,我们看一下具体的实现,伪代码如下:
while (html) {
let text, rest, next
let textEnd = html.indexOf(‘<’)
// 截取文本
if (textEnd >= 0) {
rest = html.slice(textEnd)
while (
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 如果’<'在纯文本中,将它视为纯文本对待
next = rest.indexOf(‘<’, 1)
if (next < 0) break
textEnd += next
rest = html.slice(textEnd)
}
text = html.substring(0, textEnd)
html = html.substring(textEnd)
}
// 如果模板中找不到<,那么说明整个模板都是文本
if (textEnd < 0) {
text = html
html = ‘’
}
// 触发钩子函数
if (options.chars && text) {
options.chars(text)
}
}
在代码中,我们通过while来解决这个问题(注意是里面的while)。如果剩余的模板不符合任何被解析的类型,那么重复解析文本,直到剩余模板符合被解析的类型为止。
在上面的代码中,endTag、startTagOpen、comment和conditionalComment都是正则表达式,分别匹配结束标签、开始标签、注释和条件注释。
在Vue.js源码中,截取文本的逻辑和其他的实现思路一致。
8 纯文本内容元素的处理
什么是纯文本内容元素呢?script、style和textarea这三种元素叫作纯文本内容元素。解析它们的时候,会把这三种标签内包含的所有内容都当作文本处理。那么,具体该如何处理呢?
前面介绍开始标签、结束标签、文本、注释的截取时,其实都是默认当前需要截取的元素的父级元素不是纯文本内容元素。事实上,如果要截取元素的父级元素是纯文本内容元素的话,处理逻辑将完全不一样。
事实上,在while循环中,最外层的判断条件就是父级元素是不是纯文本内容元素。例如下面的伪代码:
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
// 父元素为正常元素的处理逻辑
} else {
// 父元素为script、style、textarea的处理逻辑
}
}
在上面的代码中,lastTag代表父元素。可以看到,在while中,首先进行判断,如果父元素不存在或者不是纯文本内容元素,那么进行正常的处理逻辑,也就是前面介绍的逻辑。
而当父元素是script这种纯文本内容元素时,会进入到else这个语句里面。由于纯文本内容元素都被视作文本处理,所以我们的处理逻辑就变得很简单,只需要把这些文本截取出来并触发钩子函数chars,然后再将结束标签截取出来并触发钩子函数end。
也就是说,如果父标签是纯文本内容元素,那么本轮循环会一次性将这个父标签给处理完毕。
伪代码如下:
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
// 父元素为正常元素的处理逻辑
} else {
// 父元素为script、style、textarea的处理逻辑
const stackedTag = lastTag.toLowerCase()
const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp(‘([\s\S]?)(</’ + stackedTag + '[^>]>)’, ‘i’))
const rest = html.replace(reStackedTag, function (all, text) {
if (options.chars) {
options.chars(text)
}
return ‘’
})
html = rest
options.end(stackedTag)
}
}
上面代码中的正则表达式可以匹配结束标签前包括结束标签自身在内的所有文本。
我们可以给replace方法的第二个参数传递一个函数。在这个函数中,我们得到了参数text(代表结束标签前的所有内容),触发了钩子函数chars并把text放到钩子函数的参数中传出去。最后,返回了一个空字符串,代表将匹配到的内容都截掉了。注意,这里的截掉会将内容和结束标签一起截取掉。
最后,调用钩子函数end并将标签名放到参数中传出去,代表本轮循环中的所有逻辑都已处理完毕。
假如我们现在有这样一个模板:
当解析到script中的内容时,模板是下面的样子:
console.log(1)
此时父元素为script,所以会进入到else中的逻辑进行处理。在其处理过程中,会触发钩子函数chars和end。
钩子函数chars的参数为script中的所有内容,本例中大概是下面的样子:
chars(‘console.log(1)’)
钩子函数end的参数为标签名,本例中是script。
处理后的剩余模板如下:
9 使用栈维护DOM层级
通过前面几节的介绍,你一定会感到很奇怪,如何知道父元素是谁?
在前面几节中,我们并没有介绍HTML解析器内部其实也有一个栈来维护DOM层级关系,其逻辑与:就是每解析到开始标签,就向栈中推进去一个;每解析到标签结束,就弹出来一个。因此,想取到父元素并不难,只需要拿到栈中的最后一项即可。
同时,HTML解析器中的栈还有另一个作用,它可以检测出HTML标签是否正确闭合。例如:
在上面的代码中,p标签忘记写结束标签,那么当HTML解析器解析到div的结束标签时,栈顶的元素却是p标签。这个时候从栈顶向栈底循环找到div标签,在找到div标签之前遇到的所有其他标签都是忘记了闭合的标签,而Vue.js会在非生产环境下在控制台打印警告提示。
关于使用栈来维护DOM层级关系的具体实现思路
10 整体逻辑
前面我们把开始标签、结束标签、注释、文本、纯文本内容元素等的截取方式拆分开,单独进行了详细介绍。本节中,我们就来介绍如何将这些解析方式组装起来完成HTML解析器的功能。
首先,HTML解析器是一个函数。HTML解析器最终的目的是实现这样的功能:
parseHTML(template, {
start (tag, attrs, unary) {
// 每当解析到标签的开始位置时,触发该函数
},
end () {
// 每当解析到标签的结束位置时,触发该函数
},
chars (text) {
// 每当解析到文本时,触发该函数
},
comment (text) {
// 每当解析到注释时,触发该函数
}
})
所以HTML解析器在实现上肯定是一个函数,它有两个参数——模板和选项:
export function parseHTML (html, options) {
// 做点什么
}
我们的模板是一小段一小段去截取与解析的,所以需要一个循环来不断截取,直到全部截取完毕:
export function parseHTML (html, options) {
while (html) {
// 做点什么
}
}
在循环中,首先要判断父元素是不是纯文本内容元素,因为不同类型父节点的解析方式将完全不同:
export function parseHTML (html, options) {
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
// 父元素为正常元素的处理逻辑
} else {
// 父元素为script、style、textarea的处理逻辑
}
}
}
在上面的代码中,我们发现这里已经把整体逻辑分成了两部分,一部分是父标签是正常标签的逻辑,另一部分是父标签是script、style、textarea这种纯文本内容元素的逻辑。
如果父标签为正常的元素,那么有几种情况需要分别处理,比如需要分辨出当前要解析的一小段模板到底是什么类型。是开始标签?还是结束标签?又或者是文本?
我们把所有需要处理的情况都列出来,有下面几种情况:
-
文本
-
注释
-
条件注释
-
DOCTYPE
-
结束标签
-
开始标签
我们会发现,在这些需要处理的类型中,除了文本之外,其他都是以标签形式存在的,而标签是以<开头的。
所以逻辑就很清晰了,我们先根据<来判断需要解析的字符是文本还是其他的:
export function parseHTML (html, options) {
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf(‘<’)
if (textEnd === 0) {
// 做点什么
}
let text, rest, next
if (textEnd >= 0) {
// 解析文本
}
if (textEnd < 0) {
text = html
html = ‘’
}
if (options.chars && text) {
options.chars(text)
}
} else {
// 父元素为script、style、textarea的处理逻辑
}
}
}
在上面的代码中,我们可以通过<来分辨是否需要进行文本解析。
如果通过<分辨出即将解析的这一小部分字符不是文本而是标签类,那么标签类有那么多类型,我们需要进一步分辨具体是哪种类型:
export function parseHTML (html, options) {
while (html) {
if (!lastTag || !isPlainTextElement(lastTag)) {
let textEnd = html.indexOf(‘<’)
if (textEnd === 0) {
// 注释
if (comment.test(html)) {
// 注释的处理逻辑
continue
}
// 条件注释
if (conditionalComment.test(html)) {
// 条件注释的处理逻辑
continue
}
// DOCTYPE
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
// DOCTYPE的处理逻辑
continue
}
// 结束标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {
// 结束标签的处理逻辑
continue
}
// 开始标签
const startTagMatch = parseStartTag()
if (startTagMatch) {
// 开始标签的处理逻辑
continue
}
}
let text, rest, next
if (textEnd >= 0) {
// 解析文本
}
if (textEnd < 0) {
text = html
html = ‘’
}
if (options.chars && text) {
options.chars(text)
}
} else {
// 父元素为script、style、textarea的处理逻辑
}
}
}
关于不同类型的具体处理方式,前面已经详细介绍过,这里不再重复。
4 文本解析器
文本解析器的作用是解析文本。你可能会觉得很奇怪,文本不是在HTML解析器中被解析出来了么?准确地说,文本解析器是对HTML解析器解析出来的文本进行二次加工。为什么要进行二次加工?
文本其实分两种类型,一种是纯文本,另一种是带变量的文本。例如下面这样的文本是纯文本:
Hello Berwin
而下面这样的是带变量的文本:
Hello {{name}}
在Vue.js模板中,我们可以使用变量来填充模板。而HTML解析器在解析文本时,并不会区分文本是否是带变量的文本。如果是纯文本,不需要进行任何处理;但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。
我们介绍过,每当HTML解析器解析到文本时,都会触发chars函数,并且从参数中得到解析出的文本。在chars函数中,我们需要构建文本类型的AST,并将它添加到父节点的children属性中。
而在构建文本类型的AST时,纯文本和带变量的文本是不同的处理方式。如果是带变量的文本,我们需要借助文本解析器对它进行二次加工,其代码如下:
parseHTML(template, {
start (tag, attrs, unary) {
// 每当解析到标签的开始位置时,触发该函数
},
end () {
// 每当解析到标签的结束位置时,触发该函数
},
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
![](https://i-blog.csdnimg.cn/blog_migrate/e2656150afa0224c926eb79e3f87aafd.jpeg)
前端面试题是我面试过程中遇到的面试题,每一次面试后我都会复盘总结。我做了一个整理,并且在技术博客找到了专业的解答,大家可以参考下:
由于篇幅有限,只能分享部分面试题,完整版面试题及答案可以【点击我】阅读下载哦~
感悟
了么?准确地说,文本解析器是对HTML解析器解析出来的文本进行二次加工。为什么要进行二次加工?
文本其实分两种类型,一种是纯文本,另一种是带变量的文本。例如下面这样的文本是纯文本:
Hello Berwin
而下面这样的是带变量的文本:
Hello {{name}}
在Vue.js模板中,我们可以使用变量来填充模板。而HTML解析器在解析文本时,并不会区分文本是否是带变量的文本。如果是纯文本,不需要进行任何处理;但如果是带变量的文本,那么需要使用文本解析器进一步解析。因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。
我们介绍过,每当HTML解析器解析到文本时,都会触发chars函数,并且从参数中得到解析出的文本。在chars函数中,我们需要构建文本类型的AST,并将它添加到父节点的children属性中。
而在构建文本类型的AST时,纯文本和带变量的文本是不同的处理方式。如果是带变量的文本,我们需要借助文本解析器对它进行二次加工,其代码如下:
parseHTML(template, {
start (tag, attrs, unary) {
// 每当解析到标签的开始位置时,触发该函数
},
end () {
// 每当解析到标签的结束位置时,触发该函数
},
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-0qrwFACm-1713252749710)]
[外链图片转存中…(img-a0w8hV6u-1713252749711)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-XB95zu69-1713252749711)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
![](https://i-blog.csdnimg.cn/blog_migrate/e2656150afa0224c926eb79e3f87aafd.jpeg)
前端面试题是我面试过程中遇到的面试题,每一次面试后我都会复盘总结。我做了一个整理,并且在技术博客找到了专业的解答,大家可以参考下:
由于篇幅有限,只能分享部分面试题,完整版面试题及答案可以【点击我】阅读下载哦~
感悟
春招面试的后期,运气和实力都很重要,自己也是运气比较好,为了回馈粉丝朋友们(毕竟自己也玩了这么久哈哈哈),整理个人感悟和总结以上。最后祝愿大家能够收获理想offer!!