Vue源码解析--AST抽象语法树
一、概念介绍:在开发Vue的时候编译器会将模板语法编译成正常的HTML语法,而直接编译的时候是非常困难的,因此此时会借助AST抽象语法树进行周转,进而变为正常的HTML语法,使编译工作变得更加简单。抽象语法树的本质上是一个JS对象,Vue在审视所有HTML结构时是以字符串的新式进行的,最终将其解析为JS对象。AST抽象语法树服务于模板编译,将一种语法翻译为另一种语法。在Vue中将模板语法编译为HT
目录
一、概念介绍:
在开发Vue的时候编译器会将模板语法编译成正常的HTML语法,而直接编译的时候是非常困难的,因此此时会借助AST抽象语法树进行周转,进而变为正常的HTML语法,使编译工作变得更加简单。
抽象语法树的本质上是一个JS对象,Vue在审视所有HTML结构时是以字符串的新式进行的,最终将其解析为JS对象。AST抽象语法树服务于模板编译,将一种语法翻译为另一种语法。在Vue中将模板语法编译为HTML语法,自己作为中转站。
二、抽象语法树与虚拟DOM节点的关系:
图示:
抽象语法树的终点是渲染函数(h函数)。
渲染函数(h函数),它既是AST的产物,也是vnode(虚拟节点)的起源。h函数里面是不含指令的。
抽象语法树不会进行diff算法的并且抽象语法树不会直接生成虚拟节点,抽象语法树最终生成的是渲染函数的
三、尝试手写AST语法树:
注:项目实现环境
webpack@5,webpack-cli@3,webpack-dev-server@3
1. 识别开始结束标签(parse.js):
基本思想:使用指针和栈的思想,使用正则表达式来匹配标签。
实现思路:
- 有识别开始标签的正则表达式(识别a-z的小写字母和0-6数字组成的标签)
- 识别结束符号的正则表达式(识别闭合的标签)
- 识别文字的正则表达式(不考虑文字与标签同级的情况)
- 在while循环中遍历传入的字符串
- 定义两个栈,栈1和栈2。
当识别到一个开始的标签符号,那么就将这个标签入栈1,把空字符串入栈2。
当识别到的字符是标签内的文字,那么就将栈2栈顶这项改为文字。
当识别到结束标签,那么就将这个标签符号弹栈,就把栈2栈顶的元素push到栈二新的栈顶上的。
export default function(templateString){
//准备指针
var index = 0;
//剩余部分
var rest = '';
//开始标记
var startRegExp = /^\<([a-z]+[1-6]?)\>/;
//结束标签
var endRegExp = /^\<\/([a-z]+[1-6]?)\>/;
//识别文字
var wordRegExp = /^([^\<]+)\<\/([a-z]+[1-6]?)\>/;
//准备两个栈
var stack1 = [];
var stack2= [];
//遍历
while(index<templateString.length-1){
rest = templateString.substring(index);
//识别遍历到的这个字符,是不是一个开始标签
if(startRegExp.test(rest)){
console.log(rest.match(startRegExp));
let tag = rest.match(startRegExp)[1];
console.log("检测到开始标记",tag);
//将开始标记推入栈1中
stack1.push(tag);
//将空数组推入栈2 中
stack2.push([]);
//指针移动标签的长度+2?因为<>也占两位
index += tag.length+2;
}else if(endRegExp.test(rest)){
//识别这个字符是不是一个结束标签
//指针移动标签的长度加3,</>占三位
let tag = rest.match(endRegExp)[1];
console.log("检测到结束标记",tag);
//此时tag一定是和栈1顶部的是相同的
if(tag == stack1[stack1.length-1]){
// 弹栈
stack1.pop();
}else{
throw new Error(stack1[stack1.length-1]+"标签没有封闭");
}
index+=tag.length+3;
console.log(stack1);
console.log(stack2);
}else if(wordRegExp.test(rest)){
// 遍历到这个字符是不是文字
let word = rest.match(wordRegExp)[1];
if(!/^\s+$/.test(word)){
// 不是全是空
console.log("检测到文字",word);
}
index+=word.length;
} else{
index++;
// 标签中的文字
}
}
return templateString;
}
2. 使用栈形成AST(完善parse.js):
当识别到一个开始的标签符号,那么就将这个标签入栈1,把空字符串入栈2。
当识别到的字符是标签内的文字,那么就将栈2栈顶这项改为文字。
当识别到结束标签,那么就将这个标签符号弹栈,就把栈2栈顶的元素push到栈二新的栈顶上的。
export default function(templateString){
//准备指针
var index = 0;
//剩余部分
var rest = '';
//开始标记
var startRegExp = /^\<([a-z]+[1-6]?)\>/;
//结束标签
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);
//识别遍历到的这个字符,是不是一个开始标签
if(startRegExp.test(rest)){
let tag = rest.match(startRegExp)[1];
console.log("检测到开始标记",tag);
//将开始标记推入栈1中
stack1.push(tag);
//将空数组推入栈2 中
stack2.push({'tag':tag,'children':[]});
//指针移动标签的长度+2?因为<>也占两位
index += tag.length+2;
}else if(endRegExp.test(rest)){
//识别这个字符是不是一个结束标签
//指针移动标签的长度加3,</>占三位
let tag = rest.match(endRegExp)[1];
console.log("检测到结束标记",tag);
//此时tag一定是和栈1顶部的是相同的
let pop_tag = stack1.pop();
if(tag == pop_tag){
// 弹栈
let pop_arr = stack2.pop();
console.log(pop_arr);
if(stack2.length>0){
stack2[stack2.length-1].children.push(pop_arr);
}
}else{
throw new Error(stack1[stack1.length-1]+"标签没有封闭");
}
index+=tag.length+3;
console.log(stack1);
console.log(stack2);
}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;
} else{
index++;
// 标签中的文字
}
}
// 此时stack2就是我们之前默认放置的一项了,此时要返回这一项的children即可
return stack2[0].children[0];
}
3.识别attrs:
- 首先需要我们更改startRegExp的正则表达式,使其能够识别我们添加的attrs。
- 指针要移动长度因为添加的attrs会发生改变。
- 因为获取到的attrsString的长度可能为undefined,所以采用三元运算法给其添加赋值。
- 当前这个标签要有attrs这个属性,所以往栈二push的时候要添加上attrs属性。
- 我们通过正则获取到的attrsString是字符串但此时我们需要的是对象的形式,所以我们需要建一个parseAttrsString.js来处理我们获取到的attrsString的字符串。
parse.js:
import parseAttrsString from './parseAttrsString.js'
export default function(templateString){
//准备指针
var index = 0;
//剩余部分
var rest = '';
//开始标记
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);
//识别遍历到的这个字符,是不是一个开始标签
if(startRegExp.test(rest)){
let tag = rest.match(startRegExp)[1];
let attrsString = rest.match(startRegExp)[2];
const attrsStringLength = attrsString != null ? attrsString.length : 0;
console.log("检测到开始标记",tag);
//将开始标记推入栈1中
stack1.push(tag);
//将空数组推入栈2 中
stack2.push({'tag':tag,'children':[],'attrs':parseAttrsString(attrsString)});
//指针移动标签的长度+2?因为<>也占两位
index += tag.length+2+attrsStringLength;
}else if(endRegExp.test(rest)){
//识别这个字符是不是一个结束标签
//指针移动标签的长度加3,</>占三位
let tag = rest.match(endRegExp)[1];
console.log("检测到结束标记",tag);
//此时tag一定是和栈1顶部的是相同的
let pop_tag = stack1.pop();
if(tag == pop_tag){
// 弹栈
let pop_arr = stack2.pop();
console.log(pop_arr);
if(stack2.length>0){
stack2[stack2.length-1].children.push(pop_arr);
}
}else{
throw new Error(stack1[stack1.length-1]+"标签没有封闭");
}
index+=tag.length+3;
console.log(stack1);
console.log(stack2);
}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;
} else{
index++;
// 标签中的文字
}
}
// 此时stack2就是我们之前默认放置的一项了,此时要返回这一项的children即可
return stack2[0].children[0];
}
parseAttrsString.js:
函数功能:将获取到的字符串attrs转换成数组对象的形式。
思路:利用引号和空格将其拆分。
- 首先遍历字符串,遇到引号后将其 isStr 属性设置成true,此时在引号内遇到空格不用管,当遇到下一个引号时设置 isStr 为false。此时在 isStr为false的情况下再遇见引号就将前面的字符串截取出来放入到数组中。
- 最后将数组里面的内容利用map进行拆分。
//把attrsString变为数组返回
export default function(attrsString){
if(attrsString == undefined) return [];
// 当前是否在引号内
var isStr = false;
// 断点
var point = 0;
// 结果数组
var result = [];
// 遍历attrsString
for(let i = 0;i<attrsString;i++){
let char = attrsString[i];
if(char == '"' ){
isStr = !isStr;
}else if(char == ' ' && !isStr){
// 遇见了空格,并且不在引号内
console.log(i);
if(!/^\s*$/.test(attrsString.substring(point,i))){
result.push(attrsString.substring(point,i).trim());
point = i;
}
}
}
// 循环结束之后,最后还剩一个属性
result.push(attrsString.substring(point).trim());
result = result.map(item=>{
// 根据等号拆分
const o = item.match(/^(.+)="(.+)$/);
return{
name:o[1],
name:o[2]
}
});
return result;
}
注:学习资料《尚硅谷Vue源码系列课程》
代码地址。
更多推荐
所有评论(0)