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 () {
// 每当解析到标签的结束位置时,触发该函数
},
chars (text) {
text = text.trim()
if (text) {
const children = currentParent.children
let expression
if (expression = parseText(text)) {
children.push({
type: 2,
expression,
text
})
} else {
children.push({
type: 3,
text
})
}
}
},
comment (text) {
// 每当解析到注释时,触发该函数
}
})
在chars函数中,如果执行parseText后有返回结果,则说明文本是带变量的文本,并且已经通过文本解析器(parseText)二次加工,此时构建一个带变量的文本类型的AST并将其添加到父节点的children属性中。否则,就直接构建一个普通的文本节点并将其添加到父节点的children属性中。而代码中的currentParent是当前节点的父节点,也就是前面介绍的栈中的最后一个节点。
假设chars函数被触发后,我们得到的text是一个带变量的文本:
“Hello {{name}}”
这个带变量的文本被文本解析器解析之后,得到的expression变量是这样的:
"Hello "+_s(name)
上面代码中的_s其实是下面这个toString函数的别名:
function toString (val) {
return val == null
-
? ‘’
- typeof val === ‘object’ ? JSON.stringify(val, null, 2)
- String(val)
}
假设当前上下文中有一个变量name,其值为Berwin,那么expression中的内容被执行时,它的内容是不是就是Hello Berwin了?
我们举个例子:
var obj = {name: ‘Berwin’}
with(obj) {
function toString (val) {
return val == null
-
? ‘’
- typeof val === ‘object’ ? JSON.stringify(val, null, 2)
- String(val)
}
console.log("Hello "+toString(name)) // “Hello Berwin”
}
在上面的代码中,我们打印出来的结果是"Hello Berwin"。
事实上,最终AST会转换成代码字符串放在with中执行, 接着,我们详细介绍如何加工文本,也就是文本解析器的内部实现原理。
在文本解析器中,第一步要做的事情就是使用正则表达式来判断文本是否是带变量的文本,也就是检查文本中是否包含{{xxx}}这样的语法。如果是纯文本,则直接返回undefined;如果是带变量的文本,再进行二次加工。所以我们的代码是这样的:
function parseText (text) {
const tagRE = /{{((?:.|\n)+?)}}/g
if (!tagRE(text)) {
return
}
}
在上面的代码中,如果是纯文本,则直接返回。如果是带变量的文本,该如何处理呢?
一个解决思路是使用正则表达式匹配出文本中的变量,先把变量左边的文本添加到数组中,然后把变量改成_s(x)这样的形式也添加到数组中。如果变量后面还有变量,则重复以上动作,直到所有变量都添加到数组中。如果最后一个变量的后面有文本,就将它添加到数组中。
这时我们其实已经有一个数组,数组元素的顺序和文本的顺序是一致的,此时将这些数组元素用+连起来变成字符串,就可以得到最终想要的效果,如图9-5所示。图5 文本解析过程
在图5中,最上面的字符串代表即将解析的文本,中间两个方块代表数组中的两个元素。最后,使用数组方法join将这两个元素合并成一个字符串。
具体实现代码如下:
function parseText (text) {
const tagRE = /{{((?:.|\n)+?)}}/g
if (!tagRE.test(text)) {
return
}
const tokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index
while ((match = tagRE.exec(text))) {
index = match.index
// 先把 {{ 前边的文本添加到tokens中
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
// 把变量改成_s(x)
这样的形式也添加到数组中
tokens.push(_s(${match[1].trim()})
)
// 设置lastIndex来保证下一轮循环时,正则表达式不再重复匹配已经解析过的文本
lastIndex = index + match[0].length
}
// 当所有变量都处理完毕后,如果最后一个变量右边还有文本,就将文本添加到数组中
打开全栈工匠技能包-1小时轻松掌握SSR
两小时精通jq+bs插件开发
生产环境下如歌部署Node.js
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
网易内部VUE自定义插件库NPM集成
谁说前端不用懂安全,XSS跨站脚本的危害
webpack的loader到底是什么样的?两小时带你写一个自己loader