Vue 2 阅读理解(七)之 ParseHTML函数

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情

ParseHTML函数 - HTML 模板解析

之前在解析 parse 函数时,我们知道整个 解析 template 模板并生成 ast 对象 的过程都发生在这个函数的执行过程中。但是 parse 函数内部本身只定义了一些标签、指令的处理方法和警告函数,并且在传递给 parseHTML 函数的参数中定义了四个处理方法。最终是通过调用 parseHTML 来解析 template 模板

整个解析过程,其实就是 通过一系列正则表达式来匹配 template 模板字符串,并截取该部分匹配内容并重新匹配剩余部分,直到全部匹配完成。

所有的正则表达式包含以下内容:

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/  // 静态属性解析
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 动态属性解析(含有 v-xxx:, :, @, # 的属性)
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)   // 开始标签部分
const startTagClose = /^\s*(\/?)>/                     // 开始标签结束
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)// 结束标签部分
const doctype = /^<!DOCTYPE [^>]+>/i                   // DOCTYPE 声明
const comment = /^<!\--/                               // 注释部分
const conditionalComment = /^<!\[/                     // 条件注释
复制代码

parseHTML 的简要代码如下:

export function parseHTML(html, options: HTMLParserOptions) {
  const stack: any[] = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  let index = 0
  let last, lastTag
  while (html) {
    last = html
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
        if (comment.test(html)) {
          // 处理 注释
          advance()
          continue
        }
        if (conditionalComment.test(html)) {
          // 处理 条件注释
          advance()
          continue
        }

        // Doctype:
        if (html.match(doctype)) {
          advance()
          continue
        }

        // 结束:
        if (html.match(endTag)) {
          advance()
          parseEndTag()
          continue
        }

        // 开始:
        if (parseStartTag()) {
          advance()
          handleStartTag()
          continue
        }
      }
      
      // 处理纯文本
      options.chars()
      advance()
    } else {
      // 结束
      parseEndTag()
    }

    if (html === last) {
      options.chars && options.chars(html)
      break
    }
  }

  parseEndTag()

  function advance(n) {
  }

  function parseStartTag() {
  }

  function handleStartTag(match) {
  }

  function parseEndTag(tagName?: any, start?: any, end?: any) {
  }
}
复制代码

其中定义了三个标签处理方法和一个定位方法:

  • advance 方法是更新当前解析到的文本位置 index,并截取掉已解析的部分
  • parseStartTag 方法用来解析标签的开始部分,内部会生成一个包含标签名 tagName 和属性数组 attrs 的对象 math,并循环解析内部的字符串直到解析完整个字符串,将解析到的属性存放到 attrs 数组中
  • handleStartTag 则是处理 parseStartTag 得到的 math 对象(大部分是处理每个属性),如果上面解析到是非闭合标签的话,也会将这个 math 对象修改成一个新对象插入到 parseHTML 执行时定义的 stack 元素栈中;当然,如果是自闭合标签,还会调用 parseEndTag 方法处理;并在 最后调用 options 中定义的 start 函数来生成 ast 对象
  • parseEndTag 方法就用来处理标签结束后的逻辑,会根据条件清除 stack 栈中的元素;如果此时元素标签是 br 或者 p,也会调用 option.start(),其他情况下一般会调用 options.end() 来结束当前标签的解析

总的来说,parseHTML 就是通过正则表达式来匹配不同的标签和属性,进行不同的标签/属性处理,最后通过 options 中的回调函数来创建完整的 ast 对象;并用 stack 元素栈的方式来保证原始 template 模板与 ast 对象的层级结构的一致性。

猜你喜欢

转载自juejin.im/post/7131714599274938381