浅曦Vue源码-18-挂载阶段-$mount(6)

166 阅读5分钟

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

一、前情回顾 & 背景

上一篇小作文讨论了 parseHTML 方法中收到回调方法中的两个比较简单的方法:options.commentoptions.chars

其中 options.comment 用于处理注释,方法忽略 root 平级的注释节点,然后创建注释内容的 ast 节点,并添加到 currentParent.children 数组中;

options.chars 处理字符,根据配置的相关字符选项决定压缩与否,然后根据是否有 Vue 的动态绑定语法 {{xx}} 创建不同类型 typeast 节点,有动态绑定语法 type2,没有 type3

两个最重要的方法 options.startoptions.end 方法,这两个方法作为重中之重,而本篇小作文的重点是先了解 options.start 方法的大致逻辑。其中涉及的繁杂的细节处理方法的逻辑,将放到下一篇小作文中讨论。

二、options.start

方法位置:parse 内方法,parseHTML 方法的回调方法;

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  // ....
parseHTML(template, {
  // ...
  shouldKeepComment: options.comments,

  start (tag, attrs, unary, start: number, end: number) {
  
  }
})
  // 返回生成的 ast 对象
  return root
}

方法参数:

  1. tag: 开始标签名;
  2. attrsattrs 数组,是经过 handleStartTag 后的 attrs,形如 :[{ name: attrName, value: attrValue, start, end }, ....]
  3. unary:是否自闭和标签
  4. start:开始位置
  5. end:结束位置

方法作用: parseHTML 匹配到开始标签时调用 parseStartTag 得到 startMatch 对象,再调用 handleStartTag 方法处理 startMatch 对象,handlerStartTag 就会调用 options.start 方法生成 ast,其具体过程包含:

  1. 调用 createASTElement 方法创建 AST 节点 —— element
  2. for 循环 preTransforms,这个数组是从 parse 方法接收到的 options.modules 中提取出来的,用于处理 class、style、model,这个内容我们稍后会详细介绍;
  3. 检查当前元素是否有 v-pre 指令,存在则设置 element.pre = true
  4. 检测是否有 <pre/> 标签,有则将标识符遍历 inPre 置为 true;
  5. 如果有 v-pre 指令,调用 processRawAttrs 方法传入 element 对象;
  6. element.prcessed 不为 true 就调用 proceessFor 处理 v-forproceessIf 处理 v-ifprocessOnce 处理 v-once
  7. 处理 root,如果 root 不存在说明当前正在处理的元素就是 root
  8. 如果 unary 参数不为 true,说明是非自闭和标签,此时维护 currentParent 元素为当前 element,后面处理到它的子元素时就知道要给这个 currentParent 添加子元素;此外还要将当前 element pushstack 中,这里这个 stackparseHTML 中的 stack 不一样,parseHTML 中的 stack 收录的是开始标签的一些基本信息,而这个 options.start 方法中的 stack 存放的都是 ast 节点即 element
  9. 如果是自闭和标签即 unarytrue,则执行 closeElement(element),而非自闭和标签在 option.end 中调用 closeElment 方法;
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  warn = options.warn || baseWarn

  platformIsPreTag = options.isPreTag || no

  platformMustUseProp = options.mustUseProp || no

  platformGetTagNamespace = options.getTagNamespace || no

  const isReservedTag = options.isReservedTag || no

  maybeComponent = (el: ASTElement) => !!(
    el.component ||
    el.attrsMap[':is'] ||
    el.attrsMap['v-bind:is'] ||
    !(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
  )

  transforms = pluckModuleFunction(options.modules, 'transformNode')
  // 这个就是创建 ast 后 for 循环中药
  preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
  postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')

  delimiters = options.delimiters

  const stack = []

  const preserveWhitespace = options.preserveWhitespace !== false
  const whitespaceOption = options.whitespace

  let root

  // 当前元素的父元素
  let currentParent
  let inVPre = false
  let inPre = false
  let warned = false

  function warnOnce (msg, range) {
  
  }

  function closeElement (element) {
   
  }

  function trimEndingWhitespace (el) {
  
  }

  function checkRootConstraints (el) {
   
  }


  parseHTML(template, {
    // ....

    start (tag, attrs, unary, start, end) {
      // 获取命名空间,如果父元素有命名空间就继承父元素的命名空间,否则获取平台命名空间
      const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)

      // 给当前标签创建 AST 节点对象 
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      if (ns) {
        element.ns = ns
      }


      // 非SSR,html模板中不能有 style、script 标签
      if (isForbiddenTag(element) && !isServerRendering()) {
        element.forbidden = true
        process.env.NODE_ENV !== 'production' && warn(
          'Templates should only be responsible for mapping the state to the ' +
          'UI. Avoid placing tags with side-effects in your templates, such as ' +
          `<${tag}>` + ', as they will not be parsed.',
          { start: element.start }
        )
      }

      // 为 element 对象分别执行 class、style、model
      // 这个 preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
      // options.modules 是 creaetCompiler(baseOptions) 中 baseOptions 中配置的 modules
      // 包含三个模块:klass、style、model,
      // 但是这这三个模块在 web 下只有 model 存在 'preTransformNode' 方法,所以这里只执行
      // model.preTransformNode 方法处理 element 上的 v-model 指令
      for (let i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element
      }

      if (!inVPre) {
        // 表示 element 是否存在 v-pre 指令,
        // 存在则设置 element.pre = true
        processPre(element)
        if (element.pre) {
          // 存在 v-pre 指令,则设置 inPre 为 true
          inVPre = true
        }
      }

      // 如果当前元素时 <pre></pre> 标签,则设置 inPre 为 true
      if (platformIsPreTag(element.tag)) {
        inPre = true
      }


      if (inVPre) {
        // 说明标签上存在 v-pre 指令,将节点上 attrList 中的值赋值到到 el.attrs 数组
        processRawAttrs(element)
      } else if (!element.processed) {
       
        // 处理 v-for 属性,得到 element.for = 要迭代的对象 
        // element.alias = 别名
        processFor(element)

        // 处理 v-if v-else-if v-else
        // 得到 element.if = 'exp', element.elseif = exp, element.else = true
        // v-if 属性会额外在 element.ifConditions 数组中添加 { exp, block } 对象
        processIf(element)

        // 处理 v-once 指令,得到 element.once = true
        processOnce(element)
      }

      // 如果 root 不存在,当前 element 就是第一个元素,即组件的根元素 root
      if (!root) {
        root = element
        if (process.env.NODE_ENV !== 'production') {}
      }

      if (!unary) {
        // 非自闭和标签,维护 currentParent 记录当前元素,
        // 当处理到下一个开始标签的时候,其父元素就是 currentParent
        currentParent = element

        // 将 element push 到 stack,将来处理到当前元素的闭合标签时出栈
        stack.push(element)
      } else {
        // 说明当前元素为自闭和标签,在当前的的 start 方法中就执行 closeElment 方法
        // 而非自闭和标签则在 options.end 中执行 closeElement
        closeElement(element)
      }
    },
  })

  // 返回生成的 ast 对象
  return root
}

三、总结

本篇小作文的主题是讨论 parseHTML 方法执行过程中解析到开始标签后调用 parseHTML 方法接收到的参数options.start 回调方法处理开始标签,其主要工作如下:

  1. 创建 AST 节点 element
  2. 调用 options.modules 中的 preTransformNode 方法处理 elementoptions.modules 来之 createCompiler 时传入的 baseOptions
  3. 处理 v-pre 指令以及在 pre 标签内的情景,接着处理 v-forv-forv-ifv-once
  4. 维护 root 节点,root 只在第一次处理时会被赋值,后面处理的所有节点都是 root 的子节点;
  5. 维护 currrentParent 变量,这个意义在于:调用 parseHTML 的时候是以一种一维的方式解析树形的模板字符串,但是建立 AST 时却需要还原模板描述的节点间的父子关系,也就是说 AST 是有深度的。
  6. 非自闭和元素时维护 element 入栈 stack,当解析到闭合标签时出栈,如果是自闭合标签执行 closeElement 自动闭合当前元素,因为它没有闭合标签了,闭合标签的逻辑就要在这儿调用