「这是我参与2022首次更文挑战的第17天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
我们正在讲述 new Vue 过程即 this._init() 方法的最后一步 $mount 实施挂载,在这个过程中会将模板解析成 AST 对象,然后再将 AST 转换成 render/staticRenderFns 渲染函数。
而将模板编译成 ast 的是 parse 方法,而 parse 内部是调用 parseHTML 方法解析 html 模板,以 < 为标识,将模板分成注释、条件注释、文档声明、开始/闭合标签、普通文本类型,然后调用 parseHTML 接收到的 options 中的 comment/start/end/chars 方法处理;
上一篇小作文中主要讲解了 parse 主流程,以及 parse 的一个分支流程—— parseHTML 的主流程,本篇小作文将继续完成 parseHTML 流程中的具体方法;
export function parseHTML (html, options) {
// 现在这个代码中的代码只是个示意,大致方法的调用位置
let index = 0
let last, lastTag
while (html) {
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
advance(commentEnd + 3)
parseEndTag(endTagMatch[1], curIndex, index)
const startTagMatch = parseStartTag()
handleStartTag(startTagMatch)
options.chars(text, index - text.length, index)
parseEndTag(stackedTag, index - endTagLength, index)
options.chars && options.chars(html)
}
parseEndTag()
function advance (n) { }
function parseStartTag () {}
function handleStartTag (match) {}
function parseEndTag (tagName, start, end) {}
}
二、advance
方法位置:parseHTML 内
方法参数:n,index 要前进的长度数字
方法作用:
- 在
parseHTML中维护了index变量,每次累加n,index其实是个指针,用于记录下次需要处理的位置,这个位置是一个html模板字符串的索引; - 维护
html变量,html变量最初是parseHTML方法接收的模板字符串,随着while循环的处理index再逐渐增长,而html就需要变为剩下的未经处理的字符串,这个过程html变为原来模板的子字符串,慢慢缩短长度,到最后就把所有html都处理过了;
function advance (n) {
index += n
html = html.substring(n)
}
三、parseStartTag
方法位置:parseHTML 方法内
方法作用:解析开始标签,以 <div id="app">为例,得到描述这个开始标签的 match 对象,所谓 match 对象是一个包含由 tagName 属性描述开始标签的标签名,attrs 属性描述的开始标签的行内属性集合等属性的对象。主要分为以下几步:
- 调用
html.match方法,传入startTagOpen正则匹配出开始标签信息,如图
-
声明
match对象,其中包含tagName,attrs,start属性,tagName就是当前开始标签的标签名,attrs就是开始标签后面的行内属性,start就是parseHTML中index的值; -
调用
advance前进start[0].length,start[0]是包含div,是match方法返回的第一项 -
while循环,处理从开始标签开始处理开始标签后面的的行内属性,比如id="app",把匹配到属性塞到match.attrs中 -
在
4的while循环过程中会匹配开始标签的结束,匹配到结束时,为match对象添加end属性,调用 advance前进结束的长度,最后返回match` 对象
function parseStartTag () {
const start = html.match(startTagOpen)
if (start) {
const match = {
tagName: start[1],
attrs: [],
start: index
}
advance(start[0].length)
let end, attr
// while 循环,终止条件为:匹配到结束开始标签的结束;没有匹配到是动态参数属性或普通属性
// 开始标签内的各个属性,并将这些属性放到 match.attrs 数组中
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
attr.start = index
advance(attr[0].length)
attr.end = index
match.attrs.push(attr)
}
// 开始标签的对应结束,比如 <div id="app"> 的 > 或者自闭和标签的 />
if (end) {
match.unarySlash = end[1]
advance(end[0].length)
match.end = index
return match
}
}
}
match 对象长这样子:
总结一下:
前面说 while 循环是靠 index 作为指针,后面匹配到注释、条件注释、开始标签等调用对应方法处理,处理完都会维护 index 递增,维护 html 逐渐缩短。从 parseStartTag 方法可以看出一点痕迹,从开始标签开始解析,把有用信息从 html 字符串上提取出来,然后让 index 前进让 html 变短(这就是 advance 方法的作用),然后匹配 > 或者自闭和的 /> 匹配到结尾,开始标签就算结束了。而 index 前进了 <div id="app"> 这个字符串的长度,而 html 已经不再包含 <div id="app"> 字符串了。
四、handleStartTag 方法
方法位置:parseHTML 方法内部
方法参数:match,上一步中 parseStartTag 返回的 match 对象
方法作用:
- 解析上一步
parseStartTag返回的match对象,处理match.attrs,把match中的atts形如['id="app"', 'id', '=', 'app', undefined, undefined]变成{ name: 'id', value: 'app' }这种形式; - 如果不是自闭和标签,把标签的信息放入
stack中,以便将来处理到它的闭合标签时弹出stack,以此完成html标签的成对儿问题处理。这种处理方法和一个经典面试题多个括号闭合问题是如出一辙的,其实这也是另一个能够用while这个一维的循环处理HTML这种树形结构的关键点;而自闭和标签,也就是unary 一元标签,就直接处理标签上的信息;push到stack的对象如下,等下parseEnd的时候要用到
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLowerCase(),
attrs: attrs,
start: match.start,
end: match.end
})
- 接下来调用
options.start方法,传入tagName,attrs,match.start,match.end
function handleStartTag (match) {
const tagName = match.tagName // 标签名字
const unarySlash = match.unarySlash // 是否有自闭和的斜杠, 即 /> 中的 /
if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag)
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName)
}
}
// 一元标签,即自闭和标签,比如 \<img />
const unary = isUnaryTag(tagName) || !!unarySlash
// 处理 match.attrs,得到 attrs[i] = [{name: attrName, value: attrVal, star: xx, end: xx }, ...]
// 未经过处理的attrs 长这样:
// attrs = [['id="app"', 'id', '=', 'app', undefined, undefined], ...]
const l = match.attrs.length
const attrs = new Array(l)
for (let i = 0; i < l; i++) {
const args = match.attrs[i]
// arg[3] = 'app', args[4]= undefined, args[5]=undefined
const value = args[3] || args[4] || args[5] || ''
const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref // 如果是 a 标签,要处理 href 的值进行编码
: options.shouldDecodeNewlines
// 将 atrrs[i] 改写成 { name, value } 形式
attrs[i] = {
name: args[1],
value: decodeAttr(value, shouldDecodeNewlines)
}
// 记录非生产环境,记录属性的开始和结束索引
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length
attrs[i].end = args.end
}
}
// 如果不是自闭和标签,将标签信息放到 stack 数组中,当处理到结束标签时出栈
if (!unary) {
// 将标签信息入栈,{ tag, lowerCasedTag, attrs, start, end }
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
lastTag = tagName // 保存当前标签名到 lastTag
}
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
}
五、parseEndTag
方法位置:parseHTML 内部方法
方法参数:
tagnName:闭合标签名start:开始索引end:结束索引
方法作用:
-
定义
pos遍历,如果传递了闭合标签名tagName,就从stack栈中倒着查找,一直找到stack栈中的tagName属性和传入的tagName属性相同的项,它的索引就是pos的值,理论上不存在未闭合标签时,栈顶的项就是收到的闭合标签tagName对应的开始标签数据; -
处理
stack,不存在未闭合的元素时,栈顶就是对应的开始标签。此时找到闭合标签的stack中对应的项,让其出栈,始终保持stack中的最后一项是下一次html模板匹配到结束标签时对应的开始标签对应的信息;
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
if (start == null) start = index
if (end == null) end = index
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
}
// 如果再上面的查找闭合标签对应开始标签的 for 循序中一直没有找到它对应的开始标签,
// pos 会因为一直递减导致小于 0 而退出循环,
// 所以 pos 就会 < 0,说明没有找到开始标签,则存在未闭合的标签
if (pos >= 0) {
// 说明找到了对应的闭合标签
for (let i = stack.length - 1; i >= pos; i--) {
if (process.env.NODE_ENV !== 'production' &&
(i > pos || !tagName) &&
options.warn
) {
// 异常情况,原则上栈顶就是当前 tagName 的闭合元素
// 如果在 pos 的前面还有一些元素,说明这些元素也是没有闭合标签
options.warn(
`tag <${stack[i].tag}> has no matching end tag.`,
{ start: stack[i].start, end: stack[i].end }
)
}
if (options.end) {
// 调用 options.end 方法处理结束标签
options.end(stack[i].tag, start, end)
}
}
// 相当于出栈,pos 正常情况下就应该是 stack.length - 1,即栈顶的项,
// 当前项已经处理过了,通过修改数组 length 属性达删除某一项
stack.length = pos
// 给 lastTag 重新赋值,记录 stack 数组待处理的一个开始标签名字
// 正常情况下也是下一个闭合标签的名字才对
lastTag = pos && stack[pos - 1].tag
} else if (lowerCasedTagName === 'br') {
// 处理 <br /> 标签情景
if (options.start) {
options.start(tagName, [], true, start, end)
}
} else if (lowerCasedTagName === 'p') {
// p 标签情况
if (options.start) {
// 处理 <p> 标签
options.start(tagName, [], false, start, end)
}
if (options.end) {
// 处理 </p> 标签
options.end(tagName, start, end)
}
}
}
六、总结
本篇小作文讲述 parseHTML 过程中用到的工具方法:
advance: 维护index和html,index记录在原html中的处理位置,html逐渐缩短成没处理过的部分;parseStartTag:用正则匹配出开始标签中的tagName和attrs以及 开始标签的结束部分如>或者/>,并且入栈stack当前开始标签的信息;handleStartTag:进一步处理parsetStartTag得到的match对象,转换attrs数组;parseEndTag:当匹配到结束标签时,维护stack,使stack对应当前结束标签的开始标签出栈;