vue2-parseHTML实现

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 前言我们前面分析了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的。


相关文章
|
6月前
|
JavaScript
|
29天前
|
JavaScript iOS开发
用上Vue3,你真的变了吗?
10月更文挑战第6天
45 0
|
2月前
|
存储 缓存 JavaScript
Vue3比Vue2快在哪里?
本文分析了Vue 3相比Vue 2在性能提升的几个关键点,包括改进的diff算法、静态标记、静态提升、事件监听器缓存以及SSR渲染优化,这些改进使得Vue 3在处理大规模应用时更加高效。
36 1
|
6月前
|
JavaScript 前端开发
vue toRaw和markRaw的使用
vue toRaw和markRaw的使用
62 0
|
6月前
|
API
vue3没有this怎么办?
vue3没有this怎么办?
|
监控 JavaScript 前端开发
vue v-for
vue v-for
59 0
|
存储 数据处理
Vue3中shallowRef和shallowReactive的使用?
Vue3中shallowRef和shallowReactive的使用?
137 0
|
JavaScript API
Vue(Vue2+Vue3)——77.Vue3.0
Vue(Vue2+Vue3)——77.Vue3.0
|
JavaScript CDN 容器
1.初识Vue(2.x)
初识Vue(2.x)
108 0
|
JavaScript 开发者
vue
我们都是用vue-cli搭建环境的,因为vue的脚手架给我们配置好了绝大多数功能,如果不能满足需要,仅仅修改一下相关的脚本文件就可以了,省时省力。nodejs、vue-cli安装就不说了
83 0