目录
1. 前言
上篇文章中我们说到,在模板解析阶段主线函数parse
中,根据要解析的内容不同会调用不同的解析器,而在三个不同的解析器中最主要的当属HTML
解析器,为什么这么说呢?因为HTML
解析器主要负责解析出模板字符串中有哪些内容,然后根据不同的内容才能调用其他的解析器以及做相应的处理。那么本篇文章就来介绍一下HTML
解析器是如何解析出模板字符串中包含的不同的内容的。
2. HTML解析器内部运行流程
在源码中,HTML
解析器就是parseHTML
函数,在模板解析主线函数parse
中调用了该函数,并传入两个参数,代码如下:
// 代码位置:/src/complier/parser/index.js
/**
* Convert HTML string to AST.
* 将HTML模板字符串转化为AST
*/
export function parse(template, options) {
// ...
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
// 当解析到开始标签时,调用该函数
start (tag, attrs, unary) {
},
// 当解析到结束标签时,调用该函数
end () {
},
// 当解析到文本时,调用该函数
chars (text) {
},
// 当解析到注释时,调用该函数
comment (text) {
}
})
return root
}
从代码中我们可以看到,调用parseHTML
函数时为其传入的两个参数分别是:
- template:待转换的模板字符串;
- options:转换时所需的选项;
第一个参数是待转换的模板字符串,无需多言;重点看第二个参数,第二个参数提供了一些解析HTML
模板时的一些参数,同时还定义了4个钩子函数。这4个钩子函数有什么作用呢?我们说了模板编译阶段主线函数parse
会将HTML
模板字符串转化成AST
,而parseHTML
是用来解析模板字符串的,把模板字符串中不同的内容出来之后,那么谁来把提取出来的内容生成对应的AST
呢?答案就是这4个钩子函数。
把这4个钩子函数作为参数传给解析器parseHTML
,当解析器解析出不同的内容时调用不同的钩子函数从而生成不同的AST
。
-
当解析到开始标签时调用
start
函数生成元素类型的AST
节点,代码如下;// 当解析到标签的开始位置时,触发start start (tag, attrs, unary) { let element = createASTElement(tag, attrs, currentParent) } export function createASTElement (tag,attrs,parent) { return { type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), parent, children: [] } }
从上面代码中我们可以看到,
start
函数接收三个参数,分别是标签名tag
、标签属性attrs
、标签是否自闭合unary
。当调用该钩子函数时,内部会调用createASTElement
函数来创建元素类型的AST
节点 -
当解析到结束标签时调用
end
函数; -
当解析到文本时调用
chars
函数生成文本类型的AST
节点;// 当解析到标签的文本时,触发chars chars (text) { if(text是带变量的动态文本){ let element = { type: 2, expression: res.expression, tokens: res.tokens, text } } else { let element = { type: 3, text } } }
当解析到标签的文本时,触发
chars
钩子函数,在该钩子函数内部,首先会判断文本是不是一个带变量的动态文本,如“hello ”。如果是动态文本,则创建动态文本类型的AST
节点;如果不是动态文本,则创建纯静态文本类型的AST
节点。 -
当解析到注释时调用
comment
函数生成注释类型的AST
节点;// 当解析到标签的注释时,触发comment comment (text: string) { let element = { type: 3, text, isComment: true } }
当解析到标签的注释时,触发
comment
钩子函数,该钩子函数会创建一个注释类型的AST
节点。
一边解析不同的内容一边调用对应的钩子函数生成对应的AST
节点,最终完成将整个模板字符串转化成AST
,这就是HTML
解析器所要做的工作。
3. 如何解析不同的内容
要从模板字符串中解析出不同的内容,那首先要知道模板字符串中都会包含哪些内容。那么通常我们所写的模板字符串中都会包含哪些内容呢?经过整理,通常模板内会包含如下内容:
- 文本,例如“难凉热血”
- HTML注释,例如<!-- 我是注释 -->
- 条件注释,例如<!-- [if !IE]> -->我是注释<!--< ![endif] -->
- DOCTYPE,例如<!DOCTYPE html>
- 开始标签,例如<div>
- 结束标签,例如</div>
这几种内容都有其各自独有的特点,也就是说我们要根据不同内容所具有的不同的的特点通过编写不同的正则表达式将这些内容从模板字符串中一一解析出来,然后再把不同的内容做不同的处理。
下面,我们就来分别看一下HTML
解析器是如何从模板字符串中将以上不同种类的内容进行解析出来。
3.1 解析HTML注释
解析注释比较简单,我们知道HTML
注释是以<!--
开头,以-->
结尾,这两者中间的内容就是注释内容,那么我们只需用正则判断待解析的模板字符串html
是否以<!--
开头,若是,那就继续向后寻找-->
,如果找到了,OK,注释就被解析出来了。代码如下:
const comment = /^<!\--/
if (comment.test(html)) {
// 若为注释,则继续查找是否存在'-->'
const commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
// 若存在 '-->',继续判断options中是否保留注释
if (options.shouldKeepComment) {
// 若保留注释,则把注释截取出来传给options.comment,创建注释类型的AST节点
options.comment(html.substring(4, commentEnd))
}
// 若不保留注释,则将游标移动到'-->'之后,继续向后解析
advance(commentEnd + 3)
continue
}
}
在上面代码中,如果模板字符串html
符合注释开始的正则,那么就继续向后查找是否存在-->
,若存在,则把html
从第4位("<!--"长度为4)开始截取,直到-->
处,截取得到的内容就是注释的真实内容,然后调用4个钩子函数中的comment
函数,将真实的注释内容传进去,创建注释类型的AST
节点。
上面代码中有一处值得注意的地方,那就是我们平常在模板中可以在<template></template>
标签上配置comments
选项来决定在渲染模板时是否保留注释,对应到上面代码中就是options.shouldKeepComment
,如果用户配置了comments
选项为true
,则shouldKeepComment
为true
,则创建注释类型的AST
节点,如不保留注释,则将游标移动到'-->'之后,继续向后解析。
advance
函数是用来移动解析游标的,解析完一部分就把游标向后移动一部分,确保不会重复解析,其代码如下:
function advance (n) {
index += n // index为解析游标
html = html.substring(n)
}
为了更加直观地说明 advance
的作用,请看下图:
调用 advance
函数:
advance(3)
得到结果:
从图中可以看到,解析游标index
最开始在模板字符串的位置0处,当调用了advance(3)
之后,解析游标到了位置3处,每次解析完一段内容就将游标向后移动一段,接着再从解析游标往后解析,这样就保证了解析过的内容不会被重复解析。
3.2 解析条件注释
解析条件注释也比较简单,其原理跟解析注释相同,都是先用正则判断是否是以条件注释特有的开头标识开始,然后寻找其特有的结束标识,若找到,则说明是条件注释,将其截取出来即可,由于条件注释不存在于真正的DOM
树中,所以不需要调用钩子函数创建AST
节点。代码如下:
// 解析是否是条件注释
const conditionalComment = /^<!\[/
if (conditionalComment.test(html)) {
// 若为条件注释,则继续查找是否存在']>'
const conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
// 若存在 ']>',则从原本的html字符串中把条件注释截掉,
// 把剩下的内容重新赋给html,继续向后匹配
advance(conditionalEnd + 2)
continue
}
}
3.3 解析DOCTYPE
解析DOCTYPE
的原理同解析条件注释完全相同,此处不再赘述,代码如下:
const doctype = /^<!DOCTYPE [^>]+>/i
// 解析是否是DOCTYPE
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
advance(doctypeMatch[0].length)
continue
}
3.4 解析开始标签
相较于前三种内容的解析,解析开始标签会稍微复杂一点,但是万变不离其宗,它的原理还是相通的,都是使用正则去匹配提取。
首先使用开始标签的正则去匹配模板字符串,看模板字符串是否具有开始标签的特征,如下: