「这是我参与2022首次更文挑战的第24天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上一篇小作文继续讨论了 closeElement 的工具方法,大致如下:
-
processSlotOutlet:解析自闭和的slot标签,获取插槽名字设置到el上; -
processComponent:解析动态组件,解析is属性和inline-template -
transforms:class/style模块导出的transoformNode方法处理静态、动态的类名或样式 -
processAttrs:处理元素上的attrs,处理指令的参数、事件绑定、修饰符等;
这一篇就是 closeElment 的终结篇,本篇过后 options.start 的所有逻辑也就全部完成了。本期我们将会完成 options 最后一个回调方法:options.end,options.end 方法用于处理非自闭和标签的结束标签。
除了 options.end 方法,我们还会回顾一下 parse 方法和 parseHTML 方法的整体流程。另外,从标题看得出来,我们在探讨 Vue 的挂载逻辑,因为执行栈的深入,让 parse 变得悠长,像北方的冬季一样的漫长😘😘
二、options.end
方法位置:parseHTML 的 options 的回调方法
方法参数:
- tag:标签名
- start:开始索引位置
- end:结束索引位置
方法作用:处理结束标签,即</div>,主要工作如下:
- 获取
stack栈顶元素element,并使之出栈; - 维护
currentParent,经过上一步出栈,stack栈顶的元素就是element的parent; - 调用
closeElement完成对标签的处理,调用processElement处理节点属性,然后组织节点间父子关系就是在这个方法处理的,前文讲述自闭和标签时讲过这个方法了,这里不再赘述
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
parseHTML(template, {
start (tag, attrs, unary, start, end) {
},
end (tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
// 获取 currentParent 父节点,不是兄弟节点,
// 因为每次处理完一个节点,自己就会出栈,
// 即便有弟弟节点,当处理到弟弟节点时,哥哥节点也早已经出栈了
currentParent = stack[stack.length - 1]
if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
element.end = end
}
closeElement(element)
}
})
思考?
这个地方有点精妙的,要花一点篇幅说一说,这里大家思考一个问题,在 options.start 中我们检测到是非自闭和标签时会将当前 currentParent 设置为开始标签,但是当执行到闭合标签时却要将 currentParent 设为 stack 栈顶的元素这是为什么?
我们以这段简单的模板代码来分析一下 currentParent 的更新过程:
<div id="outer">
<span id="prev">someTxt</span>
<i id="next">someItalyTxt</i>
</div>
-
当解析到
<div时,调用options.start,为div#outer创建ast节点对象element, 然后更新currentParent为div#outer这个ast,并且让div#outer入栈stack,此时stack =[div#outer]; -
接着解析就解析到
<span了,要注意span#prev的parent是div#inner。<span又是一个开始标签,所以所以接着执行1.类似逻辑:为span#prev创建ast,更新currentParent为span#prev,并且入栈,此时stack=[div#outer, span#prev];后面处理到span内部的文案是someTxt时,调用options.chars将someTxt的文本ast加入到currentParent.children(即span#inner.children); -
继续解析就到了
</span>,此时就是一个闭合标签,此时就要调用options.end方法来处理了。首先获取栈顶元素赋值给element,element = span#prev,然后stack出栈,此时stack=[div#outer];注意了,重点就来了😂😂😂:此时栈顶就是div#outer了,接着执行currentParent = stack 栈顶元素的赋值; -
接着就是解释这么做有什么意义了。此时
currentParent为div#outer了,此时接着解析模板就要解析到<i这个i标签了啊,而i标签的父元素就恰好是div#outer了,此时执行start、end逻辑时currentParent始终能保证是一个正确的值;
简言之,start 中维护 currentParent 就是为了认定从当前开始标签以后遇到的开始标签的父元素为当前元素,例如 div#outer 之后遇到的 span#prev,span#prev 的父元素就当时 div#outer;
而 end 中维护的 currentParent 是认定从当前结束标签以后在遇到开始标签时,父元素为当前元素的父元素,也就是结束标签之后的开始标签和当前标签是兄弟关系;例如 span#prev 的结束标签之后就是 i#next 的开始标签;
三、回顾 parse 方法
在 Vue 实施挂载的步骤之一就是将 HTML 模板编译成 AST 节点对象树,而 parse 方法就是实现这个过程的方法;
3.1 parse 被调用
parse 是在调用 createCompilerCreator(baseCompile) 生成 createCompiler 时传入的 baseCompile 中调用的:
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// parse 调用
const ast = parse(template.trim(), options)
})
3.2 parse 方法
parse 方法用 parseHTML 方法解析 HTML 模板,将模板解析成 AST,通过 stack、currentParent 重新组织节点间的父子关系,最终成为一棵树,最后返回顶层的根节点 ast 对象 root;
export function parse () {
//....
parseHTML(template, {
start () {},
end () {},
chars () {}
comment () {}
})
return root
}
3.3 parseHTML 方法
parseHTML 方法是真正用来解析 HTML 模板字符串的方法,它的实现是一大亮点:
while(html),通过查找 < 这个小于号出现的索引位置 index,然后以 < 作为标识符,并且通过分析 < 后面的内容分为不同的类型:注释、条件注释、文档声明、文案、开始标签、闭合标签,随后调用自身接收到的回调方法:options.comment、options.chars、options.star、options.end 处理对应的类型;
无论命中那种类型,然后根据处理类型的内容长度更新 index,然后用 index 重新截取 html 模板并复制给 html,这样一来以 index 为指针使 html 逐渐变短,最终使得整个 html 都被处理;
这个方法的设计没有递归,而是采用了一种简单的方式来处理,这样一来提升了效率并降低了内存占用。简单来说就是用一维的 while 循环遍历一个有深度的树形结构,这个思路还是非常值得借鉴的。
3.3.1 options.start
options.start 用于处理 parseHTML 解析到的开始标签,主要工作如下:
- 创建
ast节点对象,第一个被解析到开始标签就是parse方法要返回的root节点; - 调用
preTransformNode处理有v-model指令的input标签,意在处理动态绑定type,用暴力法解决动态绑定type的input标签的渲染问题,即:type 或 v-bind:type; - 维护
stack和currentParent,使此后被解析到的开始标签的父元素为当前元素; - 如果当前标签为自闭和标签,则直接不用入栈
stack,直接调用closeElement解析该元素;
3.3.2 options.end
options.end 方法用于处理 parseHTML解析到的结束标签,主要工作如下:
- 维护
stack、currentParent使所有需要组织父子关系时确保父亲节点的正确性; - 调用
closeElement完成非自闭和标签的处理:节点属性组织父子关系;
3.3.3 options.chars
生成文本节点,插入到 currentParent.children 中
3.3.4 options.comment
生成注释节点,并插入到 current.childrend 中
四、总结
- 本篇小作文完成了
parseHTML的最后一个回调方法options.end的讨论; - 就
curerntParent在start和end中的更新作用进行了详细讨论; - 回顾了整个
parse方法的parseHTML的梗概方法及作用;