Vue原理系列之二:template模板编译为AST
AST(抽象语法树)的概念
在模板语法中(如v-for循环中)如果我们直接将模板语法编译成正常的HTLM语法是非常困难的。

所以,我们需通过AST抽象语法树,将模板语法转换成正常的HTML语法,如下图所示

那么AST抽象语法树到底是什么呢?其实AST抽象语法树本质上是一个JS对象

上面图中,用JS结构来表示HTML结构实际上就是AST抽象语法树。抽象语法树是服务于模板编译的,从一种语法翻译成另外一种语法,比如 ES6 转 ES5
知识储备
在进一步讲述如何将template转化为AST之前,我们先来做一些知识储备,假定你对正则表达式、栈、递归的概念有一些基础的了解,那么请看接下来的这道题。
试编写“智能重复”,smartRepeat函数,实现: 将’3[abc]’变成abcabcabc 将’3[2[a]2[b]]’变成aabbaabbaabb 将’2[1[a]3[b]2[3[c]4[d]]]’变成abbbcccddddcccdddabbbcccddddcccddd
看到题目,一些小伙伴们就可能就会想到用递归的方法进行编写,但是你会发现这个是一个字符串,而且这个[不知道是和哪一个 ] 进行拼接的,所以使用的递归的方法比较麻烦,不是说不可以解题,但是会很复杂。这里我们就需要使用栈的思想来解决问题。
首先需要两个栈stack1和stack2,一个用来存储数字,一个用来存储字符串。
分析了思路之后,相关代码如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| function smartRepeat(templateStr) { var index = 0 var stack1 = [] var stack2 = [] var rest = templateStr while(index < templateStr.length -1) { rest = templateStr.substring(index) if (/^\d+\[/.test(rest)) { let times = Number(rest.match(/^(\d+)\[/)[1]) stack1.push(times) stack2.push('') index += times.toString().length + 1 } else if (/^\w+\]/.test(rest)) { let word = rest.match(/^(\w+)\]/)[1] stack2[stack2.length-1] = word index += word.length } else if(rest[0] == ']'){ let times = stack1.pop() let word = stack2.pop() stack2[stack2.length -1 ] += word.repeat(times) index++ } console.log(index, stack1,stack2); } return stack2[0].repeat(stack1[0]) } console.log(smartRepeat('3[2[a]2[b]]'));
|
记住这个解题思路,因为接下来的词法分析会用到栈的思路去解决问题。
AST抽象树的手动实现
1 2 3 4 5 6 7 8 9 10 11 12 13
| import parse from "./parse"; var templateStr = `<div> <h3>你好</h3> <ul> <li>A</li> <li>B</li> <li>C</li> </ul> </div> ` let ast = parse(templateStr) console.log(ast);
|
根据以上给出的html代码,我们可以得到转换后的AST抽象语法树应为:

通过观察我们发现,其实这 AST抽象语法树 的转换和上面栈的题目几乎差不多,都是要使用两个栈来进行运算,当遇到 <>标签 时进栈,遇到 </>标签 出栈。所以我们可以借助上面的题目 栈的思路 来完成 AST抽象语法树 的转换。如果你真的理解上面的题目,这道题你理解起来很非常轻松。
parse()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
|
export default function parse(templateStr) { var index = 0 var stack1 = [] var stack2 = [{'children':[]}] var rest = templateStr var startRegExp = /^\<([a-z]+[1-6]?)\>/ var endRegExp = /^\<\/([a-z]+[1-6]?)\>/ var wordErgExp = /^([^\<]+)\<\/([a-z]+[1-6]?)\>/ while (index < templateStr.length - 1) { rest = templateStr.substring(index) if (startRegExp.test(rest)) { let tag = rest.match(startRegExp)[1] stack1.push(tag) stack2.push({'tag': tag, 'children': []}) index += tag.length + 2 }else if (endRegExp.test(rest)){ let tag = rest.match(endRegExp)[1] let pop_tag = stack1.pop() if(tag === pop_tag ) { let pop_arr = stack2.pop() if(stack2.length > 0) { stack2[stack2.length - 1].children.push(pop_arr) } } else { new Error(stack1[stack1.length - 1] + '标签没有闭合') } index += tag.length + 3 } else if(wordErgExp.test(rest)) { let word = rest.match(wordErgExp)[1] if(!/^\s+$/.test(word)) { stack2[stack2.length - 1].children.push({'text': word, 'type': 3}) } index += word.length } else { index++ } } return stack2[0].children[0] }
|
当我们往标签中添加类名的时候,其实会报如下图的错误。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import parse from "./parse"; var templateStr = `<div> <h3 class="box box1" id="h3">你好</h3> <ul> <li>A</li> <li>B</li> <li>C</li> </ul> </div> ` let ast = parse(templateStr) console.log(ast);
|

原因很简单,就是在解析标签的时候,会默认将class=”box box1” id=”h3”也当成标签来处理,所以就会报错,即我们还需要将开始标签的正则进行完善
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| var startRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/ ... if (startRegExp.test(rest)) { let tag = rest.match(startRegExp)[1] let attrsString = rest.match(startRegExp)[2] console.log('开始标记', tag); stack1.push(tag) stack2.push({'tag': tag, 'children': [],attrs: attrsString}) const attrsStringLength = attrsString != null ? attrsString.length : 0 index += tag.length + 2 + attrsStringLength ...
|

可以发现有了attrs属性,但是attrs属性是一个数组,里面包含name和value的值,所以我们需要对attrs进行改造。
1 2
| attrs:[{name:class,value:box},{name:id,value:h3}]
|
将attrs改造成数组包含对象name和value属性。

通过上面的动图思路,我们就可以通过这个思路去书写代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| export default function parseAttrsString(attrsString) { if (attrsString == undefined) return [] var isClass = false var point = 0 var result = [] for (let i = 0; i < attrsString.length; i++) { let char = attrsString[i] if (char == '"') { isClass = !isClass } else if (char == ' ' && !isClass) { if (!/^\s*$/.test(attrsString.substring(point, i).trim())) { result.push(attrsString.substring(point, i).trim()) point = i } } } result.push(attrsString.substring(point).trim()) result = result.map(item => { let obj = item.match(/^(.+)="(.+)"$/) return { name: obj[1], value: obj[2] } }) return result }
|
最后一个完美的 AST抽象语法树 就完成了,测试结果如下图所示:
