先探究下语法树是什么,是做什么的
graph TD
模版语法 --> 抽象语法树AST --> 正常的HTML语法
它本质上是一个js对象,vue会字符串的视觉看待.vue文件中的内容。将它解析为AST,下面举个例子
<div class="box">
<h3 class="title">我是一个标题</h3>
<ul>
<li v-for="item in arr" :key="index">
{{item}}
</li>
</ul>
</div>
上面的内容会被解析为下面的对象
{
tag:"div",
attrs:[{name: "class", value: "box"}],
type:1,
children:[
{
tag: "h3",
attrs:[{name: "class", value: "title"}],
type:1,
children:[{text: "我是一个标题", type: 3}]
},
{
tag: "ul",
attrs:[],
type:1,
children:[
{
tag: "li",
for: "arr",
key: "index",
alias: "item",
type: 1,
children: []
}
]
}
]
}
抽象语法树和虚拟节点的关系
graph LR
模版语法 --> 抽象语法树AST -->渲染函数h函数 -->虚拟节点 -->界面
下面对AST进行探秘
大概步骤分为以下几步
- 相关算法知识的准备。 如指针,递归, 栈
- AST形成算法
- 写AST编译器
- 写文本解析功能
- AST 优化
- 将AST生成h()函数
这里准备了三个算法题,帮助理解后面 的内容 juejin.cn/column/6997…
下面是将 字符串转为AST语法的 代码笔记
index.js
import parse from './parse.js'
var templateString = `
<div>
<h3 class="box" id="box1">你好</h3>
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
<div>
<div>大家好</div>
</div>
</div>
`
const ast = parse(templateString)
console.log(ast)
parse.js
import parseAttrString from './parseAttrsString'
export default function (templateString) {
// 创建遍历的指针
var index = 0;
// 剩余部分
var rest = "";
// 开始标记
//var startRegExp = /^\<([a-z]+[1-6]?)\>/;
// 添加上class 等attr 内容后的正则
var startRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/;
// 结束标签
var endRegExp = /^\<\/([a-z]+[1-6]?)\>/;
// 抓取结束标签前的文字
var wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/;
// 创建两个栈
var stack1 = [];
var stack2 = [{ children: [] }];
while (index < templateString.length - 1) {
rest = templateString.substring(index);
//console.log(templateString[index])
// 识别遍历到的标签是不是一个开始标签
if (startRegExp.test(rest)) {
let tag = rest.match(startRegExp)[1];
let attrString = rest.match(startRegExp)[2];
const attrLength = attrString != undefined? attrString.length: 0
console.log(attrLength)
// 将开始标签压入栈1
stack1.push(tag);
// 给栈2 压入一个数组
stack2.push({ tag: tag, children: [] , attrs: parseAttrString(attrString)});
console.log("检测到开始标签" + tag);
index += tag.length + 2 +attrLength ;
//console.log(stack1,stack2)
} else if (endRegExp.test(rest)) {
// 识别结束标签
let tag = rest.match(endRegExp)[1];
// 出栈的时候判断 栈顶是不是和当前标签一致
if (tag == stack1[stack1.length - 1]) {
let stack_pop = stack1.pop();
let stack_pop2 = stack2.pop();
stack2[stack2.length - 1].children.push(stack_pop2);
} else {
throw Error(tag + "标签没有封闭");
}
//console.log("检测到结束标签" + tag);
index += tag.length + 3;
//console.log(stack1,stack2)
} else if (wordRegExp.test(rest)) {
// 遍历到的字符是不是文字 并且不是全空
let word = rest.match(wordRegExp)[1];
if (!/^\s+$/.test(word)) {
//console.log("检测到中间文字" + word);
stack2[stack2.length - 1].children.push({ text: word, type: 3 });
}
index += word.length;
} else {
index++;
}
}
return stack2[0].children[0];
}
parseAttrsSting.js
// 把字符串返回数组 格式为 [{name: '', value: ''}]
export default function (string) {
if (string==undefined) return []
// 不能简单的使用 split 的方法, 需要使用 遍历的方式
// 创建开始的点
let point = 0;
// 创建一个索, 初始值是 false ,遇到第一个 " 后 变为 true 再次遇见 " 的时候 变为 false
// 只有当遇到空格的时候,并且 索的值为false 的时候 才分割这段字符创
let yiyinhao = false
// 定义一个存放值的数组
let arr = []
for (let i=0; i< string.length; i++) {
let char = string[i]
if (char=='"') {
yiyinhao = !yiyinhao
}else if (char==' '&& !yiyinhao) {
if (!/^\s*$/.test(string.substring(point,i))) {
arr.push(string.substring(point,i).trim())
point = i
}
}
}
arr.push(string.substring(point).trim())
let result = arr.map(item => {
// 根据等号拆分
let o = item.match(/^(.+)="(.+)"$/)
return {
name: o[1],
value: o[2]
}
})
return result
}