// 每当解析到文本时,触发该函数
},
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 () {
// 每当解析到标签的结束位置时,触发该函数
},
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
}
}
在上面的代码中,如果是纯文本,则直接返回。如果是带变量的文本,该如何处理呢?
ajax
1)ajax请求的原理/ 手写一个ajax请求?
2)readyState?
3)ajax异步与同步的区别?
4)ajax传递中文用什么方法?
在上面的代码中,我们打印出来的结果是"Hello Berwin"。
事实上,最终AST会转换成代码字符串放在with中执行, 接着,我们详细介绍如何加工文本,也就是文本解析器的内部实现原理。
在文本解析器中,第一步要做的事情就是使用正则表达式来判断文本是否是带变量的文本,也就是检查文本中是否包含{{xxx}}这样的语法。如果是纯文本,则直接返回undefined;如果是带变量的文本,再进行二次加工。所以我们的代码是这样的:
function parseText (text) {
const tagRE = /{{((?:.|\n)+?)}}/g
if (!tagRE(text)) {
return
}
}
在上面的代码中,如果是纯文本,则直接返回。如果是带变量的文本,该如何处理呢?
ajax
1)ajax请求的原理/ 手写一个ajax请求?
2)readyState?
3)ajax异步与同步的区别?
4)ajax传递中文用什么方法?
[外链图片转存中…(img-nZmaexxl-1714650766134)]
[外链图片转存中…(img-oRjM7zG4-1714650766135)]