likes
comments
collection

手写 Vue2 源码之模板解析成AST语法树

作者站长头像
站长
· 阅读数 12

前言

Vue 源码一直是前端面试必问的题目,熟悉源码能让我们在面试过程中脱颖而出,熟悉源码也能让我们在开发过程中知道整个框架是如何运转的,遇到了问题,我们能第一时间知道是哪里出了问题。

过去的章节,已经实现了源码:

  1. 实现 new Vue()
  2. 实现数组劫持

现在我们开始从数据转向视图,看看如何将 html 模板编译成 Dom 元素。

模板编译一般有这么几个步骤:

  1. 模板解析成 AST 语法树。
  2. AST 语法树转成 render 函数。
  3. render 函数构建虚拟 Dom
  4. 虚拟 Dom 转成真实 Dom

这篇文章,我们用代码实现如何将 HTML 模板转化为 AST 语法树。

AST 语法树

概念

AST 全称为 Abstract Syntax Tree,译为抽象语法树。在 JavaScript 中,任何一个对象(变量、函数、表达式等)都可以转化为一个抽象语法树的形式,是源代码语法结构的一种抽象表示。抽象语法树本质就是一个树形结构的对象。

可以在 ASTexplorer 工具上测试,看看我们的表达式转换为 AST 语法树是什么样子的。

手写 Vue2 源码之模板解析成AST语法树

JavaScript 编译执行流程

JS 执行的第一步是读取 js 文件中的字符流,然后通过词法分析生成令牌流 Tokens,之后再通过语法分析生成 AST,最后生成机器码执行。

解析过程主要分为词法分析语法分析

词法分析

词法分析也称之为扫描(scanner),指的是将对象逐个扫描,获取每一个字母的信息,生成由对象组成的一维数组。

const a = 5;
//词法分析
[{value:'const',type:'keyword'},{value:'a',type:'identifier'}...]

语法分析

语法分析指的是将有关联的对象整合成树形结构的表达形式。

const a = 5;
//语法分析
{
  "type": "Program",
  "start": 0,
  "end": 12,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 12,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 11,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 11,
            "value": 5,
            "raw": "5"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

应用

工作中每天都在使用它

  • React
    • 将JSX语法转为React.createElement语法
  • Vue
    • 将模板语法转为JavaScript代码
  • webpack、Vite、rollup
    • 依赖关系树
    • 代码打包
    • loader
  • TypeScript
    • 将TypeScript语法转为JavaScript语法
  • Sass、Less
    • 将Sass、Less语法转为CSS语法
  • Babel
    • 将 ES6+ 语法的代码转为 ES5 语法
  • ESLint、Prettier
    • 语法检查、代码格式化

操作AST的包

  • babel
    • @babel/parser
      • Code 转 AST
    • @babel/traverse
      • 遍历 AST
    • @babel/types
      • 判断节点类型和生成新节点
    • @babel/generator
      • AST 转 Code
  • acorn
  • recast
  • esprima

获取模板内容

我们先按照常规 vue 写法,写一份基本的 html 模板:

public/index.html:

<div id="app">
  <div class="person">
    <span class="person-name">{{name}}</span>
    <span class="person-age">{{age}}</span>
  </div>
</div>
<script src="../dist/vue.js"></script>
<script>
  let vm = new Vue({
    el: '#app',
    data() {
      return {
        name: 'ts',
        age: 18,
        sports: ['basketball', 'football'],
      }
    }
  })
</script>

我们需要先获取到模板内容,才能将模板解析成 AST 语法树,模板可以有几种存在的方式:

  • new Vue 里面 render 函数:

手写 Vue2 源码之模板解析成AST语法树

  • HTML 标签:

手写 Vue2 源码之模板解析成AST语法树

  • new Vue 里面 template 属性:

手写 Vue2 源码之模板解析成AST语法树

我们需要先判断是否有根元素,然后去执行挂载元素的方法 $mount

src/instance/init.js

// 判断传入的options是否有根元素
if (vm.$options.el) {
  // 执行挂载元素方法
  vm.$mount(vm.$options.el)
}

Vue 原型上添加一个 $mount 方法。

src/instance/init.js:

Vue.prototype.$mount = function (el) {}

在获取模板内容之前,我们还需要判断 el 不能为空以及不能直接是 htmlbody 标签。

src/instance/init.js:

Vue.prototype.$mount = function (el) {
  // 获取元素
  el = el && query(el)
  // 判断元素是否直接挂载到body上
  if (el === document.body || el === document.documentElement) {
    console.warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`)
    return this
  }
}

比如,当我们直接挂载到 body 上面时:

手写 Vue2 源码之模板解析成AST语法树

手写 Vue2 源码之模板解析成AST语法树

能在控制台看到我们输出的 warn 内容。

接下来,我们从 render 函数、 template 或者 el 中拿到模板内容,需要做一些 if 判断操作:

src/instance/init.js:

// 没有render函数
if (!options.render) {
  let template
  // 有template属性,采用template
  if (options.template) {
    template = options.template
  } else if (el) {
    // 没有template属性,采用el.outerHTML
    template = el.outerHTML
  }
  console.log(template, 'template')
}

我们做下测试,比如没有 template 属性:

手写 Vue2 源码之模板解析成AST语法树

手写 Vue2 源码之模板解析成AST语法树

成功拿到了模板内容。

我们加入 template 属性试试:

手写 Vue2 源码之模板解析成AST语法树

手写 Vue2 源码之模板解析成AST语法树

同样也拿到了模板内容。

下面,我们就需要将我们拿到的模板内容解析成 AST 语法树。

模板内容解析成 AST 语法树

我们在 src 文件下新建 compiler 编译文件,index.js 文件里面放入我们将模板解析成 AST 语法树的逻辑,定义一个方法 compileToFunctions 处理解析模板并对外导出,在 src/instance/init.js 里引入使用。

src/compiler/index.js:

export const compileToFunctions = (template) => {
  console.log(template)
}

src/instance/init.js:

if (template) {
  options.render = compileToFunctions(template)
}

解析过程

Vue2 中主要有三种解析器: HTML 解析器文本解析器以及过滤解析器。其中 HTML 解析器是最主要的,它的作用就是解析 template 模板。

源码位置 src/compiler/parser/index.js:

手写 Vue2 源码之模板解析成AST语法树

模板解析的过程就是不断调用函数的处理过程。使用正则表达式去匹配,匹配到不同的内容就去触发处理对应片段的方法。比如开始标签正则匹配到开始标签,触发 start 函数,函数处理匹配到的开始标签片段,生成一个标签节点添加到抽象语法树上。

正则表达式

这里准备了几个正则表达式用于匹配标签和文本等:

// 解析标签和属性的正则表达式
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ //匹配到的是属性,如 a=b,key 是匹配到第一个分组,value 的值可能是 Group 3、Group 4、Group 5 其中的一个,即第二个分组,第三个分组和第四个分组其中一个。
const startTagOpen = new RegExp(`^<${qnameCapture}`) // 匹配到的分组是标签开始部分,如:<div
const startTagClose = /^\s*(\/?)>/ //匹配到的是开始标签的结束部分,如 > 或者 />。
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) // 匹配到的分组是标签结束部分,如 </div>
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // 匹配到的是我们表达式的内容,如 {{ name }}

可以结合正则可视化看看主要是匹配哪内容:

startTagOpen:

手写 Vue2 源码之模板解析成AST语法树

匹配到的是标签开始部分,如 <div

endTag:

手写 Vue2 源码之模板解析成AST语法树

匹配到的是标签结束部分,如 </div>

attribute:

手写 Vue2 源码之模板解析成AST语法树

匹配到的是属性,如 a=b,属性 keyvalue 之间可以存在空格, value 可以使单引号、双引号和没有引号等。key 是匹配到第一个分组,value 的值可能是 Group 3Group 4Group 5 其中的一个,即第二个分组,第三个分组和第四个分组其中一个。

startTagClose:

手写 Vue2 源码之模板解析成AST语法树

匹配到的是开始标签的结束部分,如 > 或者 />

defaultTagRE:

手写 Vue2 源码之模板解析成AST语法树

这个也比较简单,匹配到的是我们表达式的内容,如 {{ name }}

下面,我们介绍最重要的解析器 - HTML 解析器

HTML 解析器

解析 HTML 模板的过程就是 while 循环的过程,简单来说就是用 HTML 模板字符串来循环,每轮循环都从 HTML 模板中截取一小段字符串,然后重复以上过程,直到 HTML 模板被截成一个空字符串时结束循环,解析完毕。

源码位置 src/compiler/parser/html-parser.js:

手写 Vue2 源码之模板解析成AST语法树

这里举一个简单的 HTML 模板来模拟解析的过程:

  <div>
    <div>ts</div>
  </div>

第一次循环,正则匹配到开始标签,截取 <div>,触发 start 方法,剩下模板为:


  <div>ts</div>
</div>

第二次循环,正则匹配到空文本,截取出一段换行空字符串,触发 chars 方法,剩下模板为:

  <div>ts</div>
</div>

第三次循环,正则匹配到开始标签,截取 <div>,触发 start 方法,剩下模板为:

 ts</div>
</div>

第四次循环,正则匹配到文本 ts,截取 ts,触发 chars 方法,剩下模板为:

 </div>
</div>

第五次循环,正则匹配到结束标签,截取 </div>,触发 end 方法,剩下模板为:


</div>

第六次循环,正则匹配到空文本,截取出一段换行空字符串,触发 chars 方法,剩下模板为:

</div>

第七次循环,正则匹配到结束标签,截取 </div>,触发 end 方法,剩下模板为:

第八次循环,发现只有一个空字符串,解析完毕,循环结束。

这就是大致的解析流程,下面我们开始结合代码实现。

截取模板

前面已经了解了解析过程,现在我们可以开始对模板进行 while 循环然后匹配不同的正则进行处理。

HTML 开始肯定是以 <`` 开头,所以我们可以根据 < 的位置判断目前解析到的是什么位置。

src/compiler/index.js:

function parseHTML(html) {
  let textEnd = html.indexOf('<')
  if (textEnd === 0) {

  }
  if (textEnd > 0) {

  }
  if (textEnd < 0) {
    
  }
}

用变量 textEnd 表示 < 第一次出现的位置:

  • textEnd === 0,表示的是标签开始位置或者结束位置,如 <div>hello world</div> 或者 </div>
  • textEnd > 0,表示的是文本结束位置,如 hello world</div>
  • textEnd < 0,表示的是文本,如 hello world

基于这三种条件,我们可以对解析进一步处理。

textEnd === 0

对标签开始位置或者结束位置,我们将处理逻辑放在 parseStartTag 方法里面:

function parseStartTag() {
  const start = html.match(startTagOpen)
  console.log(start)
  if (start) {
    const match = {
      tagName: start[1],
      attrs: []
    }
    console.log(match)
    advance(start[0].length)
    console.log(html)
  }
}

parseStartTag 方法中,我们得到了开始标签匹配的内容 start:

手写 Vue2 源码之模板解析成AST语法树

start[0] 就是标签开始部分,start[1] 为标签名。然后将 start 内容组装到变量 match 里面,属性有标签名 tagName 和属性 attrsattrs 我们后面会处理,暂时放个空数组,这里根据需要,还可以放一些别的属性。

手写 Vue2 源码之模板解析成AST语法树

接着,我们需要将匹配到的模板字符串进行截取掉,然后进入下一个循环,截取字符串放在了 advance 方法中。

function advance(n) {
  html = html.substring(n)
}

手写 Vue2 源码之模板解析成AST语法树

这样,我们就成功将匹配到的开始标签内容 <div 截取掉了。

接下来,开始处理匹配属性 id="app",当正则匹配的不是开始标签的结束部分如 > 或者 /> 时,我们需要一直匹配属性。我们需要将属性用变量保留下来,放在前面我们定义 matchattrs 里。

let end, attr
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
  advance(attr[0].length)
  match.attrs.push({
    name: attr[1],
    value: attr[3] || attr[4] || attr[5]
  })
}

这里我们同样是需要用 advance 方法将匹配到的属性部分截取掉。打印一下目前的 htmlmatch:

手写 Vue2 源码之模板解析成AST语法树

手写 Vue2 源码之模板解析成AST语法树

可以看到打印出来的 html 已经将属性 id="app" 截取掉了,放到了 match 里面的 attrs 上。

我们还需要将 html 前面的 > 去掉:

if (end) {
  advance(end[0].length)
}

手写 Vue2 源码之模板解析成AST语法树

我们处理完了匹配到标签开始位置这种情况,textEnd === 0 还有一种情况就是匹配到标签结束位置,这种情况比较简单,我们直接讲结束标签从模板里面截取掉就行。

const endTagMatch = html.match(endTag)
if (endTagMatch) {
  advance(endTagMatch[0].length)
  continue
}

这样我们截取完了 textEnd === 0 的所有情况。

textEnd > 0 和 textEnd < 0

这里我将 textEnd > 0textEnd < 0 放在一起处理,是因为两种情况都是表示需要截取掉是的文本内容。

textEnd > 0 表示文本结束位置如: 文本内容...</div>,我们需要截取内容为'文本内容...'即 < 之前的内容。

textEnd < 0 表示的是内容全是文本如: 文本内容...,没有 > ,我们需要截取掉所有内容。

两种情况都比较简单,所以放在一起处理:

let text
// 表示的是文本结束位置,截取到文本结束位置
if (textEnd > 0) {
  text = html.substring(0, textEnd)
}
// 表示的是内容全是文本,将内容全部都截取掉
if (textEnd < 0) {
  text = html
}
if (text) {
  advance(text.length)
}

到目前位置,textEnd 所有情况都已经做了截取模板处理,这里贴一下到目前为止 parseHTML 方法的代码:

function parseHTML(html) {
  while (html) {
    let textEnd = html.indexOf('<')
    // 表示的是标签开始位置或者结束位置
    if (textEnd === 0) {
      // 如果是标签开始位置
      const startTagMatch = parseStartTag()
      if (startTagMatch) {
        continue
      }
      // 如果是标签结束位置
      const endTagMatch = html.match(endTag)
      if (endTagMatch) {
        advance(endTagMatch[0].length)
        continue
      }
    }
    let text
    // 表示的是文本结束位置,截取到文本结束位置
    if (textEnd > 0) {
      text = html.substring(0, textEnd)
    }
    // 表示的是内容全是文本,将内容全部都截取掉
    if (textEnd < 0) {
      text = html
    }
    if (text) {
      advance(text.length)
    }
  }
  console.log(html)
  // 截取掉被匹配到的模板部分
  function advance(n) {
    html = html.substring(n)
  }
  function parseStartTag() {
    // 匹配开始标签
    const start = html.match(startTagOpen)
    if (start) {
      // 定义match 存放标签名和属性
      const match = {
        tagName: start[1],
        attrs: []
      }
      // 截取掉匹配到的开始标签部分
      advance(start[0].length)
      let end, attr
      // 匹配属性,只要不是开始标签的结束部分,就一直匹配
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        // 截取掉匹配到的属性部分
        advance(attr[0].length)
        // 将属性添加到match下的attrs里
        match.attrs.push({
          name: attr[1],
          value: attr[3] || attr[4] || attr[5]
        })
      }
      // 如果有匹配到开始标签的结束部分,需要截取掉
      if (end) {
        advance(end[0].length)
      }
      return match
    }
    return false
  }
}

手写 Vue2 源码之模板解析成AST语法树

打印,可以看到,经过截取处理最后剩下的 html 为空,while 循环结束。

到这里,我们实现了模板的截取,当然我们这里只做了最主要的标签、文本的截取,Vue2 源码里面还做了注释 Comment文本类型 Doctype 等的截取。最后还需要将得到的这些截取掉的标签以及属性等转成 AST 语法树。

处理解析 AST

我们需要定义处理开始标签、结束标签以及文本的方法,将匹配到的模板内容转成 AST 语法树。

// 开始标签处理转化ast语法树
function start(tag, attrs) {}
// 结束标签处理转化ast语法树
function end(tag) {}
// 文本处理转化ast语法树
function charts(text) {}

我们需要得到的 AST 语法树的结构是这样:

{
    type,
    tag,
    attrsList,
    parent: {},
    children: []
}

字段解释:

  • type 标签的类型,用数字表示,如:1表示元素类型,3表示普通文本类型。
  • tag 标签名称,用字符串表示,如:'div'。
  • attrsList 属性集合,用数组表示,如:[{id, age, name}]。
  • parent 父元素,用对象表示。
  • children 子元素,用数组表示。

知道了树结构之后,我们应该怎么去创建这种数结构的数据?难点主要是如何确定元素之间的父子关系。

stack

我们可以定义一个变量 stack,它表示一个栈,作用是存储开始标签。它的进出遵循遇到开始标签进栈,遇到结束标签,对应的开始标签出栈原则。

进出栈过程图示

比如我们的模板是这样,为了便于分别,第一个 span 标签我们用 span1 表示,第二个用 span2 表示:

<div>
  <span>{{name}}</span>
  <span>{{age}}</span>
</div>

我们进行 while 循环时,匹配到开始标签就入栈,pushstack 里面,然后将当前匹配到开始的标签作为父元素,遇到结束标签就出栈。

这里,我们首先匹配到标签开始 <div>,截取掉然后入栈到 stack,将现在的 div 作为父元素同时也是根元素:

手写 Vue2 源码之模板解析成AST语法树

然后匹配到第一个 <span>,将 span1 当做 div 子元素,然后把 span1 作为当前的父元素:

手写 Vue2 源码之模板解析成AST语法树

然后匹配到文本 {{name}},将文本作为 span1 的子元素:

手写 Vue2 源码之模板解析成AST语法树

继续匹配到结束标签 </span>,遇到结束标签就将 span1 出栈,这时候我们就重新选择父元素,再次将 div 作为父元素。

手写 Vue2 源码之模板解析成AST语法树

继续匹配到开始标签 <span>,将 span2 进栈并当做 div 子元素,然后把 span2 作为当前的父元素:

手写 Vue2 源码之模板解析成AST语法树

然后匹配到文本 {{age}},将文本作为 span2 的子元素:

手写 Vue2 源码之模板解析成AST语法树

继续匹配到结束标签 </span>,遇到结束标签就将 span2 出栈,这时候我们就重新选择父元素,再次将 div 作为父元素。

手写 Vue2 源码之模板解析成AST语法树

继续匹配到结束标签 </div>,遇到结束标签就将 <div> 出栈,这时候栈里面已经没有内容了,while 循环也结束了。

手写 Vue2 源码之模板解析成AST语法树

创建生成 AST 语法树的方法:

function createASTElement(tag, attrs) {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    parent: null,
    children: []
  }
}

还需要声明栈 stack,当前父元素 currentParent 以及根节点 root

let stack = [], root, currentParent;

下面就开始根据构建树的过程去用代码实现。

start 方法

function start(tag, attrs) {
  let node = createASTElement(tag, attrs)
  if (!root) {
    root = node
  }
  if (currentParent) {
    node.parent = currentParent
    currentParent.children.push(node)
  }
  stack.push(node)
  currentParent = node
}

start 方法会生成具有 AST 结构的 node 节点,然后判断是否有根节点 root,没有说明当前元素就是根节点,将当前元素赋值给 root。接着判断是否有父元素,如果有父元素,将父元素关联到当前元素的父元素同时给父元素的 children 赋值 node,随后将元素入栈并将当前元素作为父元素。

end 方法

function end(tag) {
  stack.pop()
  currentParent = stack[stack.length - 1]
}

end 方法处理比较简单,将 stack 最后一个元素出栈,继续将栈中最后一个元素作为父元素。

chars 方法

function charts(text) {
  text = text.replace(/\s/g, '')
  text &&
    currentParent.children.push({
      type: 3,
      text,
      parent: currentParent
    })
}

遇到文本内容,直接放在父元素的 children 里面就行了。

最后,我们写一个 html 模板来测试一下:

<div id="app">
  <div class="person">
    <span class="person-name">{{name}}</span>
    <span class="person-age">{{age}}</span>
  </div>
</div>

手写 Vue2 源码之模板解析成AST语法树

这样我们就完成了整个模板解析成 AST 语法树的流程,贴一下完整代码:

src/compiler/index.js:

// 解析标签和属性的正则表达式
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ //匹配到的是属性,如 a=b,key 是匹配到第一个分组,value 的值可能是 Group 3、Group 4、Group 5 其中的一个,即第三个分组,第四个分组和第五个分组其中一个。
const startTagOpen = new RegExp(`^<${qnameCapture}`) // 匹配到的分组是标签开始部分,如:<div
const startTagClose = /^\s*(\/?)>/ //匹配到的是开始标签的结束部分,如 > 或者 />。
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) // 匹配到的分组是标签结束部分,如 </div>
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // 匹配到的是我们表达式的内容,如 {{ name }}

export const compileToFunctions = (template) => {
  const ast = parseHTML(template)
}

let stack = [],
  root,
  currentParent
function parseHTML(html) {
  // 创建AST结构元素
  function createASTElement(tag, attrs) {
    return {
      type: 1, // 1表示元素, 3表示普通文本
      tag,
      attrsList: attrs,
      parent: null,
      children: []
    }
  }
  // 开始标签处理转化ast语法树
  function start(tag, attrs) {
    // 生产AST结构的元素
    let node = createASTElement(tag, attrs)
    // 没有根节点说明当前元素就是根节点,将当前元素赋值给root
    if (!root) {
      root = node
    }
    // 如果有父元素,将父元素关联到当前元素的父元素
    if (currentParent) {
      node.parent = currentParent
      // 给父元素的children赋值node
      currentParent.children.push(node)
    }
    // 将元素入栈
    stack.push(node)
    // 将当前元素作为父元素
    currentParent = node
  }
  // 结束标签处理转化ast语法树
  function end(tag) {
    // 将stack最后一个元素出栈
    stack.pop()
    // 将栈中最后一个元素作为父元素
    currentParent = stack[stack.length - 1]
  }
  // 文本处理转化ast语法树
  function charts(text) {
    // 文本去空
    text = text.replace(/\s/g, '')
    // 如果是文本就直接放在父元素的children里面
    text &&
      currentParent.children.push({
        type: 3,
        text,
        parent: currentParent
      })
  }
  while (html) {
    let textEnd = html.indexOf('<')
    // 表示的是标签开始位置或者结束位置
    if (textEnd === 0) {
      // 如果是标签开始位置
      const startTagMatch = parseStartTag()
      if (startTagMatch) {
        start(startTagMatch.tagName, startTagMatch.attrs)
        continue
      }
      // 如果是标签结束位置
      const endTagMatch = html.match(endTag)
      if (endTagMatch) {
        end(endTagMatch[1])
        advance(endTagMatch[0].length)
        continue
      }
    }
    let text
    // 表示的是文本结束位置,截取到文本结束位置
    if (textEnd > 0) {
      text = html.substring(0, textEnd)
    }
    // 表示的是内容全是文本,将内容全部都截取掉
    if (textEnd < 0) {
      text = html
    }
    if (text) {
      charts(text)
      advance(text.length)
    }
  }
  console.log(root)
  // 截取掉被匹配到的模板部分
  function advance(n) {
    html = html.substring(n)
  }
  function parseStartTag() {
    // 匹配开始标签
    const start = html.match(startTagOpen)
    if (start) {
      // 定义match 存放标签名和属性
      const match = {
        tagName: start[1],
        attrs: []
      }
      // 截取掉匹配到的开始标签部分
      advance(start[0].length)
      let end, attr
      // 匹配属性,只要不是开始标签的结束部分,就一直匹配
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        // 截取掉匹配到的属性部分
        advance(attr[0].length)
        // 将属性添加到match下的attrs里
        match.attrs.push({
          name: attr[1],
          value: attr[3] || attr[4] || attr[5]
        })
      }
      // 如果有匹配到开始标签的结束部分,需要截取掉
      if (end) {
        advance(end[0].length)
      }
      return match
    }
    return false
  }
}

总结

模板解析是 Vue 模板编译的第一步,主要是将模板解析成 AST 语法树,生成 AST 语法树的核心就是解析 html,解析 html 则是通过不停地正则表达式匹配不同的内容,然后执行相应的处理函数,在处理函数里面会将匹配到的片段解析生成不同的节点。

源码地址github.com/liy1wen/min…