Bootstrap

【Vue2.x 源码学习】第十三篇 - 生成 ast 语法树 - 正则说明

一,前言

上篇,主要介绍了 vue 数据渲染核心流程,涉及以下几个点:

  • Vue 核心渲染流程回顾

  • 三种模板写法及优先级

  • 两种数据挂载方式

  • Vue 的原型方法 $mount

  • compileToFunction -> parserHTM流程说明

本篇,生成 ast 语法树-正则说明

二,模板解析

1,模板解析的说明

上文说到,compileToFunction 是 Vue 模板编译的入口:

export function compileToFunction(template) {
  // 1,将模板变成 AST 语法树
  let ast = parserHTML(template);
  // 2,使用 AST 生成 render 函数
  let code = generate(ast);
}

compileToFunction主要做了以上两件事:

而将 html 模板编译为 ast 语法树,就是用 js 对象的树形结构来描述 HTML 语法;

这里需要对 html 模板进行解析,而解析的方式就是使用正则不断进行匹配和处理;

2,模板的解析方式

使用正则对 html 模板进行顺序解析和处理

每处理完一段,就将处理完的这部分截取掉

就这样不停的进行解析和截取,直至将整个模板全部解析完毕


abcdefg
开始标签:
abcdefg
文本:abcdefg
开始标签:
结束标签:
结束标签:

可以使用 while 循环,对模板不停截取,直至全部解析完毕

function parserHTML(html) {
  while(html){
		// todo...
  }
}

标签还是文本?

三,正则说明

1,Vue2 相关的正则

// 标签名 a-aaa
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;  
// 命名空间标签 aa:aa-xxx
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
// 结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 
// 匹配属性 const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配标签结束的 > const startTagClose = /^\s*(\/?)>/; // 匹配 {{ }} 表达式 const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

2,匹配标签名

匹配标签名 aa-xxx

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;  

正则解析

测试匹配结果

let reg = new RegExp(ncname);
console.log(reg)  // 	/[a-zA-Z_][\-\.0-9_a-zA-Z]*/
console.log(reg.test('a-aaa')) // true	任意小写字符 a-z,中间有-,后面可以方字符

3,命名空间标签

命名空间标签:aa:aa-xxx

const qnameCapture = `((?:${ncname}\\:)?${ncname})`;

正则解析

正则解析:
(?:${ncname}\\:)?
	?: - 表示匹配但是不捕获
	后面可以有一个冒号
  ? 可有可无
  如:aa:
${ncname} 标签名
	如:aa:aa-xxx	此类命名空间标签使用较少

4,匹配开始标签-开始部分

// 匹配标签名(索引1):

5,匹配结束标签

// 匹配标签名(索引1): 
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 
console.log(endTag) // /^<\/((?:[a-zA-Z_][\-\.0-9_a-zA-Z]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Z]*)[^>]*>/

正则解析:
^<\\/		<符号开头,后面跟一个/
${qnameCapture}[^>]		中间可以放很多但不能是>
*>	最后必须要有一个>
                
// 测试匹配结果:
console.log(''.match(endTag))
[
  '',
  'aa:aa-xxxdsadsa', 		// 结束标签的标签名
  index: 0,
  input: '',
  groups: undefined
]

6,匹配属性

// 匹配属性(索引 1 为属性 key、索引 3、4、5 其中一直为属性值):aaa="xxx"、aaa='xxx'、aaa=xxx
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; 

正则解析:
^\s*  n个空格开头(0 个或 n 个)
[^\s"'<>\/=]+
 	^\s	不是空格
	^\s"'<>\/=	不是空格,不是尖脚号,不是反引号的 n 个字符
?:\s*(=)\s*
  空格和空格之间可以夹一个=等号
?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)
	不是空格,可能是单引号、可能是双引号、可能没有引号

// 情况 1:双引号的情况,aaa="xxx"
console.log('aaa="xxx"'.match(attribute))
[
  'aaa="xxx"',
  'aaa',
  '=',
  'xxx',
  undefined,
  undefined,
  index: 0,
  input: 'aaa="xxx"',
  groups: undefined
]
// 此时,索引3是有值的(xxx),4、5是undefined

// 情况 2:单引号的情况,aaa='xxx',会匹配到下一个位置
console.log(`aaa='xxx'`.match(attribute))
[
  "aaa='xxx'",
  'aaa',
  '=',
  undefined,
  'xxx',
  undefined,
  index: 0,
  input: "aaa='xxx'",
  groups: undefined
]
// 此时,会匹配到索引 4,即第二个位置

// 情况 3:没有引号的情况,aaa=xxx,第三个位置就是不带单引号的
console.log('aaa=xxx'.match(attribute))
[
  'aaa=xxx',
  'aaa',
  '=',
  undefined,
  undefined,
  'xxx',
  index: 0,
  input: 'aaa=xxx',
  groups: undefined
]
// 索引3、4是undefined,5 是有值的(xxx),表示匹配到了最后一位

应用:
属性的key:[1]
属性的值:[3]||[4]||[5]    索引 3、4、5 哪个有值取哪个

7,匹配开始标签-闭合部分

// 匹配结束标签:>
const startTagClose = /^\s*(\/?)>/;

正则解析:

^\s* 	空格 n 个
(\/?)>	尖角号有以下两种情况
	/>	自闭合
  >		没有/的闭合

8,匹配 {{ }} 表达式

// 匹配 {{   xxx    }} ,匹配到 xxx
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g

四,结尾

又成功水了一篇,还剩 8 篇

本篇,主要介绍了生成 ast 语法树-正则说明部分,涉及以下几个点:

下一篇,生成 ast 语法树-代码实现