浅曦Vue源码-23-挂载阶段-$mount(12)-options.end

310 阅读6分钟

「这是我参与2022首次更文挑战的第24天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

上一篇小作文继续讨论了 closeElement 的工具方法,大致如下:

  1. processSlotOutlet:解析自闭和的 slot 标签,获取插槽名字设置到 el 上;

  2. processComponent:解析动态组件,解析 is 属性和 inline-template

  3. transformsclass/style 模块导出的 transoformNode 方法处理静态、动态的类名或样式

  4. processAttrs:处理元素上的 attrs,处理指令的参数、事件绑定、修饰符等;

这一篇就是 closeElment 的终结篇,本篇过后 options.start 的所有逻辑也就全部完成了。本期我们将会完成 options 最后一个回调方法:options.endoptions.end 方法用于处理非自闭和标签的结束标签。

除了 options.end 方法,我们还会回顾一下 parse 方法和 parseHTML 方法的整体流程。另外,从标题看得出来,我们在探讨 Vue 的挂载逻辑,因为执行栈的深入,让 parse 变得悠长,像北方的冬季一样的漫长😘😘

二、options.end

方法位置:parseHTMLoptions 的回调方法

方法参数:

  1. tag:标签名
  2. start:开始索引位置
  3. end:结束索引位置

方法作用:处理结束标签,即</div>,主要工作如下:

  1. 获取 stack 栈顶元素 element,并使之出栈;
  2. 维护 currentParent,经过上一步出栈,stack 栈顶的元素就是 elementparent
  3. 调用 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>
  1. 当解析到 <div 时,调用 options.start,为 div#outer 创建 ast 节点对象 element, 然后更新 currentParentdiv#outer 这个 ast,并且让 div#outer 入栈 stack,此时 stack =[div#outer]

  2. 接着解析就解析到 <span 了,要注意 span#prevparentdiv#inner<span 又是一个开始标签,所以所以接着执行1.类似逻辑:为 span#prev 创建 ast,更新 currentParentspan#prev,并且入栈,此时 stack=[div#outer, span#prev];后面处理到 span 内部的文案是 someTxt 时,调用 options.charssomeTxt 的文本 ast 加入到 currentParent.children(即 span#inner.children);

  3. 继续解析就到了 </span>,此时就是一个闭合标签,此时就要调用 options.end 方法来处理了。首先获取栈顶元素赋值给 elementelement = span#prev ,然后 stack 出栈,此时 stack=[div#outer];注意了,重点就来了😂😂😂:此时栈顶就是 div#outer 了,接着执行 currentParent = stack 栈顶元素的赋值;

  4. 接着就是解释这么做有什么意义了。此时 currentParentdiv#outer 了,此时接着解析模板就要解析到 <i 这个 i 标签了啊,而 i 标签的父元素就恰好是 div#outer 了,此时执行 start、end 逻辑时 currentParent 始终能保证是一个正确的值;

简言之,start 中维护 currentParent 就是为了认定从当前开始标签以后遇到的开始标签的父元素为当前元素,例如 div#outer 之后遇到的 span#prevspan#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 解析到的开始标签,主要工作如下:

  1. 创建 ast 节点对象,第一个被解析到开始标签就是 parse 方法要返回的 root 节点;
  2. 调用 preTransformNode 处理有 v-model 指令的 input 标签,意在处理动态绑定 type,用暴力法解决动态绑定 typeinput 标签的渲染问题,即 :type 或 v-bind:type
  3. 维护 stackcurrentParent,使此后被解析到的开始标签的父元素为当前元素;
  4. 如果当前标签为自闭和标签,则直接不用入栈 stack,直接调用 closeElement 解析该元素;

3.3.2 options.end

options.end 方法用于处理 parseHTML解析到的结束标签,主要工作如下:

  1. 维护 stack、currentParent 使所有需要组织父子关系时确保父亲节点的正确性;
  2. 调用 closeElement 完成非自闭和标签的处理:节点属性组织父子关系;

3.3.3 options.chars

生成文本节点,插入到 currentParent.children

3.3.4 options.comment

生成注释节点,并插入到 current.childrend

四、总结

  1. 本篇小作文完成了 parseHTML 的最后一个回调方法 options.end 的讨论;
  2. curerntParentstartend 中的更新作用进行了详细讨论;
  3. 回顾了整个 parse 方法的 parseHTML 的梗概方法及作用;