抽象语法树(AST)也是Vue的核心内容,也不仅仅限于Vue,如在Babel中的运用,在Vue中它也是虚拟节点的上一步,虚拟节点就是通过抽象语法树得来的,但是它很简单,因为它不需要去做diff,节点的diff是通过虚拟节点进行的!
dom更新的过程:
简写Vue抽象语法树, 抽象语法树我们手写的话也就两段代码: 1.扫描模板生成AST语法树 2.改变attrs结构
1.扫描模板生成AST语法树:
在写代码前,我们应该要了解栈的概念:什么是栈?先进去,后出来。
<div class='abc' id='efg' ref='hijk'> <ul> <li>苹果</li> <li>橘子</li> <li>菠萝</li> </ul></div>
指针扫描的思想在Vue中可以说是很多地方都用到了,如抽象语法树,虚拟dom的diff算法等。
这里是运用到了指针与正则,正则不会的话也没有关系,不影响生成抽象语法树的思路,毕竟我们这个也是一个最简版的!
我们需要两个栈 : stack1 = [] , stack2 = [{ children: [] }]
这里扫描模板时,会有3个判断:
example: <div id='123' class='456'>你好</div>
1.扫描到标签开始,如:<div id='123'> div入stack1栈 , 语法树:{'tag':'div','children':[],'attrs':{ id='123' class='456'} }入stack2栈
2.扫描到标签中间的内容,如:你好 ; (文本)stack2栈顶加入文本节点 {'text':'你好','type':3}
3.扫描到标签结束,如:</div> stack1栈顶出栈, stack2栈顶出栈并移到stack2新的栈顶的children中(出栈了它下一项就是新的栈顶了,这样就可以保证语法树的结构了)
import attrsParse from './attrsParse.js'
export default function (htmlStr) {
//指针
let index = 0;
//栈1 存放开始标签名称
let stack1 = [];
//栈2 存放标签中内容
let stack2 = [{ children: [] }];
//开始标签正则 例如:<div class='abc' id='efg'>
let startRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/;
//结束标签正则 </div>
let endRegExp = /^\<\/([a-z]+[1-6]?)\>/;
//获取文字正则 文字是在 (文字)</...> ^\([^\<]+) 不是<的文字
let wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/;
while (index < htmlStr.length -1 ) {
//rest 剩余未遍历的字符
let rest = htmlStr.substring(index);
//匹配开始标记
if (startRegExp.test(rest)) {
//开始标记节点名称
let tagName = rest.match(startRegExp);
//入栈 例如:div
stack1.push(tagName[1]);
let attrs = tagName[2];
//入栈 按照ast格式 attrsParse()解析attrs
stack2.push({ 'tag': tagName[1], 'children': [], 'attrs': attrsParse(tagName[2]) });
//<>占两个字符 + class 与 id 的长度
let attrsLen = 0;
if(attrs){
attrsLen = attrs.length;
}
index = index + tagName[1].length + 2 + attrsLen;
} else if (endRegExp.test(rest)) {
//匹配结束标记
//出栈
stack1.pop();
let endTag = rest.match(endRegExp)[1];
//出栈入新栈顶
let stact2_pop = stack2.pop();
stack2[stack2.length - 1].children.push(stact2_pop);
// </>占三个字符
index = index + endTag.length + 3;
}else if( wordRegExp.test(rest) ){
//是文字
let word = rest.match(wordRegExp)[1];
// stack2 栈顶添加文字节点
//如果word不全是空字符串
if( /\S/g.test(word)){
stack2[stack2.length - 1].children.push({'text':word,'type':3});
}
index= index + word.length;
}else{
index++;
}
}
//返回栈顶数据 只剩一项 直接stack2[0]也可
return stack2[stack2.length - 1].children;
}
然后我们再改变下attrs的结构就行了: " id='123'| class='456'|" 这里也是扫描,扫到双数 ' 就截取到数组中。
export default function (attrs) {
// AST的attrs [{name:'',value:''}] 的形式 现在是这样的 " class='abc' id='efg' ref='hijk'"
//继续使用扫描
if (attrs == undefined) return [];
let point = 0;
let isYinHao = true;
//定义一个数组存放attrs
let attrsList = [];
for (let i = 0; i < attrs.length; i++) {
//'abc' 第一个引号 false ,第二个就是 true
if (attrs[i] == "'") {
isYinHao = !isYinHao;
}
//不是引号并且为 空字符时,就收集一次
if (isYinHao && attrs[i] == "'") {
attrsList.push(attrs.substring(point, i + 1));
//修改指针的位置为当前i+1 '的位置加1 下一个数据
point = i + 1;
}
}
//收集完成,将数据转换格式
attrsList = attrsList.map(e => {
e = e.trim();
let item = {
name: e.split('=')[0],
value: e.split('=')[1].replace(/'/g,"") //去掉引号,因为有两层 ""
};
return item;
});
return attrsList;
}
完事了,就两块代码,我们继续测试一下吧:
import parse from './parse.js';
let template = `<div class='abc' id='efg' ref='hijk'>
<ul>
<li>
苹果
</li>
<li>
橘子
</li>
<li>
菠萝
</li>
</ul>
</div>`
const ast = parse(template);
console.log(ast);
打印的结果:
[
{
"tag": "div",
"children": [
{
"tag": "ul",
"children": [
{
"tag": "li",
"children": [
{
"text": "\n 苹果\n ",
"type": 3
}
],
"attrs": []
},
{
"tag": "li",
"children": [
{
"text": "\n 橘子\n ",
"type": 3
}
],
"attrs": []
},
{
"tag": "li",
"children": [
{
"text": "\n 菠萝\n ",
"type": 3
}
],
"attrs": []
}
],
"attrs": []
}
],
"attrs": [
{
"name": "class",
"value": "abc"
},
{
"name": "id",
"value": "efg"
},
{
"name": "ref",
"value": "hijk"
}
]
}
]