「这是我参与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
的梗概方法及作用;