浅曦Vue源码-17-挂载阶段-$mount(5)

1,053 阅读5分钟

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

一、前情回顾 & 背景

上一篇小作文讲述 parseHTML 过程中用到的工具方法的具体逻辑;

  1. advance: 维护 indexhtmlindex 记录在原 html 中的处理位置,html 逐渐缩短成没处理过的部分;
  2. parseStartTag:用正则匹配出开始标签中的 tagNameattrs 以及 开始标签的结束部分如 > 或者 />,并且入栈 stack 当前开始标签的信息;
  3. handleStartTag:进一步处理 parsetStartTag 得到的 match 对象,转换 attrs 数组;
  4. parseEndTag:当匹配到结束标签时,维护 stack,使 stack 对应当前结束标签的开始标签出栈;

前面一再强调的是 parseHTML 会在 while(html) 的循环中以 < 作为标志将模板分为注释、条件注释、文档声明、普通文本、开始标签、结束标签,然后调用 options.commentoptions.charsoptions.startoptions.end 方法分别处理对应的内容类型,将其转化为固定类型的 AST 节点。

本篇小作文将围绕 options 上传入的方法展开,之前一直没有讲这部分,是因为看源码的时候各种方法的来回调用,加上 js 更是回调方法的行家里手,就更让人摸不到头脑。正式是因为 opitons 是在 parse 方法内调用 parseHTML 时传入的回调方法,让人更加蒙圈。

虽然我主张看源码时按照代码的执行顺序串行组织小作文,但是也要分场景,这里就明显不适用这个办法。即便如此,我还会以老方法 —— 先回顾调用及传参,然后讲方法位置、参数、作用。

二、options.comment

方法位置:parse 方法内调用 parseHTML 时传入的回调方法,如下:

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

  parseHTML(template, {
    warn,
    // ....
    outputSourceRange: options.outputSourceRange,

    comment (text: string, start, end) {
     
    }
  })

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

方法参数:

  1. text,注释文本
  2. start:起始索引位置
  3. end:结束索引位置

方法作用:

  1. 根据 currentParent 存在与否决定当前的注释节点是否为与根节点同级,与根节点同级的注释会被忽略,currentParent 不存在说明与根节点 root 平级;
  2. currentParent 存在时,创建 AST 节点,将其 pushcurrentParent 中的 children中
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  // ....

  parseHTML(template, {
    warn,
    // ....
    outputSourceRange: options.outputSourceRange,

    comment (text: string, start, end) {
        // 禁止添加任何节点作为 root 的兄弟节点,注释虽然被允许,但是创建  ast 的时候会被忽略
        if (currentParent) {
        
          // 这就是注释的 ast 节点,isComment: true,注释内容就是 text
          const child: ASTText = {
            type: 3,
            text,
            isComment: true
          }
          if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
            // 非生产环境时 ast 节点的开始索引和结束索引
            child.start = start
            child.end = end
          }
          // 将当前注释节点放到节点的 children 中
          currentParent.children.push(child)
        }
    }
  })

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

三、options.chars

方法位置:parse 方法内调用 parseHTML 时传入的回调方法,如下:

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

  chars (text: string, start: number, end: number) {
  
  }
})
  // 返回生成的 ast 对象
  return root
}

方法参数:

  1. text,文本 string
  2. start,起始索引位置
  3. end,结束索引位置

方法作用:

  1. 如果当前文本没有 currentParent,说明文本没有父元素,忽略并提示在 root 元素以外的文字
  2. 接下来分情况处理 text 情况:是否在 pre 标签内,是否压缩换行等操作
  3. 经过第二步后 text 不为空,不再 pre 中,压缩连续空格,然后创建文本的 ast 节点对象,如果文本中有 Vue 的模板绑定语法 {{xxx}} 这种, asttype2,普通的文本的 ast 对象的 type3
  4. 将上面生成的文本 ast pushchildren
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
 
  parseHTML(template, {
    // 
    chars (text: string, start: number, end: number) {
      // currentParent 不存在,说明这段文本没有父元素,是在 root 元素以外的元素
      if (!currentParent) {
        if (process.env.NODE_ENV !== 'production') {
           // 提示 root 元素外的文本
        }
        return
      }
      // IE textarea placeholder bug

      // 获取当前父元素的所有孩子节点数组
      const children = currentParent.children

      // 对 text 进行处理,
      // 删除空白字符或者者存在 whitespaceOptions 选项,则 text 赋值为空字符串或者空格
      if (inPre || text.trim()) {
        // 文本在 pre 标签内或者 trim 后不为空
        text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
      } else if (!children.length) {
        // 代码如果这行到这里,
        // 说明 text 不在 pre 标签中 text.trim() 为空,且当前父元素没有孩子节点
        // 则将 text 值为空
        text = ''
      } else if (whitespaceOption) {
        // 压缩
        if (whitespaceOption === 'condense') {
          // in condense mode, remove the whitespace node if it contains
          // line break, otherwise condense to a single space
          // 在压缩模式下,移除包含换行符的空白节点,否则压缩成一个空格
          text = lineBreakRE.test(text) ? '' : ' '
        } else {
          text = ' ' // 空格
        }
      } else {
        text = preserveWhitespace ? ' ' : ''
      }

      // 经历前面处理 text 不为空
      if (text) {
        if (!inPre && whitespaceOption === 'condense') {
          // 不在 pre 标签中且配置项中存 condense 配置,将多个连续空格压缩为单个
          // condense consecutive whitespaces into single space
          text = text.replace(whitespaceRE, ' ')
        }
        let res

        // 基于 text 生成 ast 对象
        let child: ?ASTNode
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          // 文本中存在表达式,即有 {{}} 这种 Vue 的数据绑定语法
          child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          }
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          // 纯文本节点
          child = {
            type: 3,
            text
          }
        }

        // child 创建成功,将 child 放到父元素的子元素们即 children 中,
        // 即 push 进 currentParent.children 数组
        if (child) {
          if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
            child.start = start
            child.end = end
          }
          children.push(child)
        }
      }
    },

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

四、总结

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

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

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

后面还有两个最重要的方法 options.startoptions.end 方法,这两个方法作为重中之重,所以将作为单独的篇幅出现。