vue2-parseHTML实现

简介: 前言我们前面分析了vue的编译入口,算是个开端。从今天开始我们进入到编译的主流程中分析其实现逻辑,实际编译主要分为三步,其中第一步就是生成AST。在源码中,生成AST是从解析template模板开始的,所以我们今天从parseHTML开始。

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

前言


我们前面分析了vue的编译入口,算是个开端。从今天开始我们进入到编译的主流程中分析其实现逻辑,实际编译主要分为三步,其中第一步就是生成AST。在源码中,生成AST是从解析template模板开始的,所以我们今天从parseHTML开始。

parse


我们还是从三部曲的入口开始讲起比较好,在compiler/parser/index

const ast = parse(template.trim(), options)
复制代码


parse中可以分为两个方面来分析

  1. 调用parseHTML解析template
  2. parseHTML在解析的过程中调用parse中的钩子函数生成AST
parseHTML(template, {
  warn,
  expectHTML: options.expectHTML,
  // ...
  start (tag, attrs, unary, start, end) {
    // 解析开始标签调用的钩子
  },
  end (tag, start, end) {
    // 解析结束标签调用的钩子
  },
  chars (text: string, start: number, end: number) {
    // 解析文本节点调用的钩子
  },
  comment (text: string, start, end) {
    // 解析注释节点调用的钩子
  }
})
复制代码

parseHTML


我们前面分析了parseHTML的入口,和其配置options来源,现在我们开始分析其实现,了解初步解析tempalte的实现。


我们从实例出发,假设有以下代码片段

<div class="dd" @click="xxx" :class="1" style="color: red;">
  hello
  <p>Dom</p>
</div>
复制代码


进到html-parser.js文件,我们可以先看看有以下正则定义

// 属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 动态属性 例如指令/bind/事件
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 标签名
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
// 注释节点
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!\--/
// 条件节点
const conditionalComment = /^<!\[/
复制代码


其实我们本篇文章的目的就是在于弄清解析的原理,在这里其实已经可以看到个大概了,在compile中即是借助这些强大的正则表达式去匹配字符串来完成解析的,我们下面再继续看看到底是如何做的。

export function parseHTML (html, options) {
  // 1
  const stack = []
  // 2
  let index = 0
  let last, lastTag
  // 3
  while (html) {
    last = html
    // Make sure we're not in a plaintext content element like script/style
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
        // Comment:
        // ...
        // 4
        // End tag:
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }
        // 5
        // Start tag:
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)
          }
          continue
        }
      }
      // 6
      let text, rest, next
      if (textEnd >= 0) {
        // ...
        text = html.substring(0, textEnd)
      }
      if (textEnd < 0) {
        text = html
      }
      if (text) {
        advance(text.length)
      }
      // 7
      if (options.chars && text) {
        options.chars(text, index - text.length, index)
      }
    } else {
      // ... 
    }
    // ...
  }
  // Clean up any remaining tags
  parseEndTag()
}
复制代码
// 用于不断移除已匹配数据
// 类似前进
function advance (n) {
  index += n
  html = html.substring(n)
}
复制代码


我将代码简化剩下以上的关键部分,去掉了一些标签的解析,例如注释标签,条件显示标签及有内容的scriptstyle等。接下来通过剩余的代码,我们来理解解析template的实现逻辑即达到目的。


  1. 定义了stack数组,用于存放我们解析到的标签数据。实际是作为个节点栈来使用。
  2. 变量index用于存放当前匹配位置,last存放剩余字符串,lastTag存放上次匹配标签
  3. while循环,在这我们可以发现,template的解析实际是通过不断地匹配当前字符串html头部得到的。在匹配头部后,截取头部匹配数据进行相关处理。然后再不断地裁剪html得到未匹配的字符串。

  4. 匹配结束标签如div>进行处理。

  5. 匹配为开始标签如<div class="dd" @click="xxx" :class="1" style="color: red;"进行处理。
  6. 匹配文本如hello进行处理

  7. 调用options.chars也就是在parse函数中传入的钩子对文本内容进一步解析。

通过我们前面的分析,其实可以明白parseHTML中的工作是对html进行初步解析,其实现是通过不断地使用正则匹配html的头部字符串并对其分类处理,然后再移除匹配数据继续后面的匹配。


开始标签



我们上面主要分析了如何匹配,但是没有进行匹配后的分析,我们先看看对于匹配开始标签后的处理。

function parseStartTag () {
  const start = html.match(startTagOpen)
  if (start) {
    // 1
    const match = {
      tagName: start[1],
      attrs: [],
      start: index
    }
    // 2
    advance(start[0].length)
    // 3
    let end, attr
    while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
      attr.start = index
      advance(attr[0].length)
      attr.end = index
      match.attrs.push(attr)
    }
    // ..
  }
}
复制代码


parseStartTag的逻辑比较简单,主要是以下几步


  1. 匹配到开始标签,为其创建标签对象用于存放数据

  2. 匹配后通过advance将起点前进

  3. 将匹配标签对应的节点属性,不断提取键值信息放在match.attrs


在调用parseStartTag得到基本的节点数据后,还会调用handleStartTag对其进行进一步处理

function handleStartTag (match) {
  const tagName = match.tagName
  const unarySlash = match.unarySlash
  const unary = isUnaryTag(tagName) || !!unarySlash
  // 1
  const l = match.attrs.length
  const attrs = new Array(l)
  for (let i = 0; i < l; i++) {
    const args = match.attrs[i]
    const value = args[3] || args[4] || args[5] || ''
    attrs[i] = {
      name: args[1],
      value: decodeAttr(value, shouldDecodeNewlines)
    }
  }
  // 2
  if (!unary) {
    stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
    lastTag = tagName
  }
  // 3
  if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end)
  }
}
复制代码


handleStartTag主要逻辑可以分为以下几步


  1. 处理匹配的attrs,提取键值数据

  2. 往我们开始定义的stack栈中存入节点数据

  3. 调用parse中的钩子对数据进行进一步的解析生成AST

处理结束标签


function parseEndTag (tagName, start, end) {
  let pos, lowerCasedTagName
  if (start == null) start = index
  if (end == null) end = index
  if (tagName) {
    // 1
    lowerCasedTagName = tagName.toLowerCase()
    for (pos = stack.length - 1; pos >= 0; pos--) {
      if (stack[pos].lowerCasedTag === lowerCasedTagName) {
        break
      }
    }
  } else {
    pos = 0
  }
  if (pos >= 0) {
    for (let i = stack.length - 1; i >= pos; i--) {
      if (options.end) {
        // 2
        options.end(stack[i].tag, start, end)
      }
    }
    stack.length = pos
    lastTag = pos && stack[pos - 1].tag
  }
}
复制代码


处理结束标签相对而言会比较简单


  1. 通过tagName在stack寻找对应的开始标签
  2. 在stack中寻找到开始标签后,获取其tag并调用结束标签匹配钩子

处理文本节点


对文本的解析就更加简单了,因为我们在这不会去处理文本插值的处理只是单纯获取开发者的文本例如{{text}}。所以就是简单的匹配,匹配前进,再调用对应的钩子函数进一步解析。


let text, rest, next
if (textEnd >= 0) {
  text = html.substring(0, textEnd)
}
if (text) {
  advance(text.length)
}
if (options.chars && text) {
  options.chars(text, index - text.length, index)
}
复制代码


结语


相信通过本篇文章的学习,我们可以弄清楚模板的处理解析是如何完成的,也就是parseHTML函数的主要实现逻辑。后面我们继续分析parse的另一部分,也就是我们在初步解析中调用的钩子实现。了解是如何通过那些钩子函数去最终生成AST的。


相关文章
|
3天前
|
JavaScript
vue3高雅的使用useDialog
vue3高雅的使用useDialog
66 0
|
3天前
|
Web App开发 缓存 JavaScript
Vue3 五天速成(上)
Vue3 五天速成(上)
15 2
|
3天前
|
JavaScript
除了 Vue.use(VeeValidate),还有哪些方法可以在 Vue 中使用 VeeValidate?
除了 Vue.use(VeeValidate),还有哪些方法可以在 Vue 中使用 VeeValidate?
17 0
|
3天前
|
缓存 JavaScript 前端开发
hello Vue
hello Vue
29 6
|
10月前
|
缓存 JavaScript 前端开发
vue:vue2与vue3的区别
vue:vue2与vue3的区别
263 0
|
3天前
|
JavaScript
vue
vue
44 0
|
6月前
|
资源调度 JavaScript Linux
VUE使用总结
VUE使用总结
30 0
|
8月前
|
JavaScript 前端开发 API
Vue3-介绍
Vue3-介绍
53 0
|
9月前
|
存储 数据处理
Vue3中shallowRef和shallowReactive的使用?
Vue3中shallowRef和shallowReactive的使用?
|
11月前
|
JSON JavaScript 前端开发
Vue(Vue2+Vue3)——18.收集表单数据
Vue(Vue2+Vue3)——18.收集表单数据