抽象语法树是什么
抽象语法树本质上是一个JS对象,模板语法直接编译成正常的HTML语法是非常困难的,这就要借助抽象语法树过渡,让编译工作变得简单
抽象语法树就服务于模板编译,只要涉及到翻译,一种语法翻译成另一种语法,就要借助抽象语法树
手写实现AST抽象语法树
定义两个栈和指针来辅助我们来实现AST抽象语法树
-
检测到开始标记,将标记tag放入栈1,将ast语法形式的对象{'tag':tag,'children':[]}放入栈2
-
抓取结束标记之前的文字,文字不能全为空,改变栈2的栈顶内容为抓取到的文字
-
检测到结束标记,如果tag和栈1 的顶部内容相同,栈1出栈,栈2出栈,让栈2的children加入栈2出栈的内容
// parse函数,主函数
export default function parse(templateString){
// 指针
var index = 0;
// 剩余字符串
var rest = ''
// 开始标记
var startRegExp = /^\<([a-z]+[0-6]?)\>/;
// 结束标记
var endRegExp = /^\<\/([a-z]+[0-6]?)\>/
// 抓取结束标记之前的文字
var wordRegExp = /^([^\<]+)\<\/[a-z]+[0-6]?\>/;
// 两个栈
var stack1 = [];
// 到最后stack2出栈,输出结果会为空,可以补一项
var stack2 = [{'children':[]}];
while(index < templateString.length-1){
rest = templateString.substring(index);
if(startRegExp.test(rest)){
let tag = rest.match(startRegExp)[1]
//将开始标记存入栈1中
stack1.push(tag);
// 将对象存入栈2中
stack2.push({'tag':tag,'children':[]})
// 指针移动标签的长度加2,因为<>占两位
index += tag.length +2;
// console.log('开始标记',tag)
}else if(endRegExp.test(rest)){
let tag = rest.match(endRegExp)[1];
//此时,tag一定是和栈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{
throw new Error(pop_tag + '标签没有封闭')
}
// console.log('结束标记' +tag)
// 指针移动标签的长度加2,因为</>占3位
index += tag.length +3;
}else if(wordRegExp.test(rest)){
// 识别是不是文字,不能全为空
let word = rest.match(wordRegExp)[1];
if(!/^\s+$/.test(word)){
// 文字不全是空
// console.log('检查到文字'+word)
// 改变此时stack2栈顶元素中
stack2[stack2.length-1].children.push({"text":word,"type":3})
}
index += word.length;
// console.log(stack1,JSON.stringify(stack2))
}else{
index++;
}
}
// 之前默认放置一项了,就要去第0项输出
return stack2[0].children[0]
}
测试
import parse from './parse'
var templateString = `
<div>
<h3>你好</h3>
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
</div>
`
const ast = parse(templateString)
console.log(ast)
输出结果
接下来识别attrs,当在div放入class时,上面的代码是识别不了的,但是标签和class之间肯定有一个空格,我们就要在开始标记上识别空格
var startRegExp = /^\<([a-z]+[0-6]?)(\s[^\<]+)?\>/;
指针需要移动attrsString的位数,并在对象中加入attrs属性
接下来定义parseAtrrsString函数将attrs处理成数组,并在attrs属性中调用函数
stack2.push({ 'tag': tag, 'children': [] ,'attrs':parseAtrrsString(attrsString)})
// 让attrsString变成数组返回
export default function (attrsString) {
if (attrsString === undefined) return []
// 当前是否在引号内
var isYinhao = false;
// 断点
var point = 0;
// 结果数组
var result = [];
// 遍历attrsString,而不是用split(),因为用split(' ')会导致class="aa bb cc"这种情况出现错误
for (let i = 0; i < attrsString.length; i++) {
let char = attrsString[i];
if (char === '"') {
isYinhao = !isYinhao;
console.log(isYinhao)
} else if (char === ' ' && !isYinhao) {
// 遇见了空格并且不再引号中
// 不全是空格才push进结果数组
if (!/^\s*$/.test(attrsString.substring(point, i))) {
// 去掉头尾空格
result.push(attrsString.substring(point, i).trim());
point = i;
}
}
}
// 循环结束之后还剩一个属性
result.push(attrsString.substring(point).trim())
// 返回 name=name,value=value这种形式,需要映射
result = result.map(item =>{
//根据等号拆分
const o = item.match(/^(.+)=(.+)$/);
return {
name:o[1],
value:o[2]
}
})
return result
}
输出结果