四、AST抽象语法树
2021-04-14 本文已影响0人
强某某
抽象语法树是什么
1.png抽象语法树本质上就是一个js对象,大概原理就是代码字符串一个个字符的分析从而生成AST;例如mustache模板引擎中的tokens就是AST和vnode的简化版
22.png
3.png 3.png抽象语法树服务于模板编译:把一个语言翻译成另一个语言
h函数不包含指令,说白了,ast是为了生成类似h函数的函数,去处理模板指令,生成正常的不包含指令的html(也可以处理其他语言)对应的h函数;而之前的mustache模板引擎只是铺垫知识
算法储备
- 指针思想
image.png实际上就是索引下标
// 试寻找字符串中,连续重复次数最多的字符。
var str = 'abbbccc';
// 指针
var i = 0;
var j = 1;
// 当前重复次数最多的次数
var maxRepeatCount = 0;
// 重复次数最多的字符串
var maxRepeatChar = '';
// 当i还在范围内的时候,应该继续寻找
while (i <= str.length - 1) {
// 看i指向的字符和j指向的字符是不是不相同
if (str[i] != str[j]) {
// console.log('报!!!' + i + '和' + j + '之间的文字连续相同!!都是字母' + str[i] + '它重复了' + (j - i) + '次');
// 和当前重复次数最多的进行比较
if (j - i > maxRepeatCount) {
// 如果当前文字重复次数(j - i)超过了此时的最大值
// 就让它成为最大值
maxRepeatCount = j - i;
// 将i指针指向的字符存为maxRepeatChar
maxRepeatChar = str[i];
}
// 让指针i追上指针j
i = j;
}
// 不管相不相同,j永远要后移
j++;
}
// 循环结束之后,就可以输出答案了
console.log(maxRepeatChar + '重复了' + maxRepeatCount + '次,是最多的连续重复字符');
- 递归1
// 试输出斐波那契数列的前10项,即1、1、2、3、5、8、13、21、34、55
// 缓存对象:添加缓存避免重复计算
var cache = {};
// 创建一个函数,功能是返回下标为n的这项的数字
function fib(n) {
// 判断缓存对象中有没有这个值,如果有,直接用
if (cache.hasOwnProperty(n)) {
return cache[n];
}
// 缓存对象没有这个值
// 看下标n是不是0或者是不是1,如果是,就返回常数1
// 如果不是,就递归
var v = n == 0 || n == 1 ? 1 : fib(n - 1) + fib(n - 2);
// 写入缓存。也就是说,每算一个值,就要把这个值存入缓存对象。
cache[n] = v;
return v;
}
for (let i = 0; i <= 9; i++) {
console.log(fib(i));
}
- 递归2
// 转换数组的形式[1, 2, 3, [4, 5]]要变为这样的对象:
// {
// chidren: [
// {
// value: 1
// },
// {
// value: 2
// },
// {
// value: 3
// },
// {
// children: [
// {
// {
// value: 4
// },
// {
// value: 5
// }
// }
// ]
// }
// ]
// }
// 测试数组
var arr = [1, 2, 3, [4, 5], [[[6], 7, 8], 9], 10];
// 转换函数,写法1
// function convert(arr) {
// // 准备一个结果数组
// var result = [];
// // 遍历传入的arr的每一项
// for (let i = 0; i < arr.length; i++) {
// // 如果遍历到的数字是number,直接放进入
// if (typeof arr[i] == 'number') {
// result.push({
// value: arr[i]
// });
// } else if (Array.isArray(arr[i])) {
// // 如果遍历到的这项是数组,那么就递归
// result.push({
// children: convert(arr[i])
// });
// }
// }
// return result;
// }
// 转换函数写法2,参数不是arr这个词语,而是item,意味着现在item可能是数组,也可能是数字
// 即,写法1的递归次数要大大小于写法2。因为写法2中,遇见什么东西都要递归一下。
function convert(item) {
if (typeof item == 'number') {
// 如果传进来的参数是数字
return {
value: item
};
} else if (Array.isArray(item)) {
// 如果传进来的参数是数组
return {
children: item.map(_item => convert(_item))
};
}
}
var o = convert(arr);
console.log(o);
-
栈的题目
image.png
// 试编写“智能重复”smartRepeat函数,实现:
// 将3[abc]变为abcabcabc
// 将3[2[a]2[b]]变为aabbaabbaabb
// 将2[1[a]3[b]2[3[c]4[d]]]变为abbbcccddddcccddddabbbcccddddcccdddd
function smartRepeat(templateStr) {
// 指针
var index = 0;
// 栈1,存放数字
var stack1 = [];
// 栈2,存放临时字符串
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('');
// 让指针后移,times这个数字是多少位就后移多少位加1位。
// 为什么要加1呢?加的1位是[。
index += times.toString().length + 1;
} else if (/^\w+\]/.test(rest)) {
// 如果这个字符是字母,那么此时就把栈顶这项改为这个字母
let word = rest.match(/^(\w+)\]/)[1];
stack2[stack2.length - 1] = word;
// 让指针后移,word这个词语是多少位就后移多少位
index += word.length;
} else if (rest[0] == ']') {
// 如果这个字符是],那么就①将stack1弹栈,②stack2弹栈,③把字符串栈的新栈顶的元素重复刚刚弹出的那个字符串指定次数拼接到新栈顶上。
let times = stack1.pop();
let word = stack2.pop();
// repeat是ES6的方法,比如'a'.repeat(3)得到'aaa'
stack2[stack2.length - 1] += word.repeat(times);
index++;
}
console.log(index, stack1, stack2);
}
// while结束之后,stack1和stack2中肯定还剩余1项。返回栈2中剩下的这一项,重复栈1中剩下的这1项次数,组成的这个字符串。如果剩的个数不对,那就是用户的问题,方括号没有闭合。
return stack2[0].repeat(stack1[0]);
}
var result = smartRepeat('3[2[3[a]1[b]]4[d]]');
console.log(result);
正则补充
image.pngimage.png
代码
- index.js
import parse from './parse.js';
var templateString = `<div>
<h3 class="aa bb cc" data-n="7" id="mybox">你好</h3>
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
</div>`;
const ast = parse(templateString);
console.log(ast);
- parse.js
import parseAttrsString from './parseAttrsString.js';
// parse函数,主函数:生成抽象语法树
//本质就是:前面的储备案例的算法;一个个字符的移动,同时配合正则已经出栈入栈等,形成数据结构
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);
// console.log(templateString[index]);
if (startRegExp.test(rest)) {
// 识别遍历到的这个字符,是不是一个开始标签
let tag = rest.match(startRegExp)[1];
let attrsString = rest.match(startRegExp)[2];
// console.log('检测到开始标记', tag);
// 将开始标记推入栈1中
stack1.push(tag);
// 将空数组推入栈2中
stack2.push({ 'tag': tag, 'children': [], 'attrs': parseAttrsString(attrsString) });
// 得到attrs字符串的长度
const attrsStringLength = attrsString != null ? attrsString.length : 0;
// 指针移动标签的长度加2再加attrString的长度,为什么要加2呢?因为<>也占两位
index += tag.length + 2 + attrsStringLength;
} else if (endRegExp.test(rest)) {
// 识别遍历到的这个字符,是不是一个结束标签
let tag = rest.match(endRegExp)[1];
// console.log('检测到结束标记', tag);
let pop_tag = stack1.pop();
// 此时,tag一定是和栈1顶部的是相同的
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 + '标签没有封闭!!');
}
// 指针移动标签的长度加3,为什么要加2呢?因为</>也占3位
index += tag.length + 3;
} else if (wordRegExp.test(rest)) {
// 识别遍历到的这个字符,是不是文字,并别不能是全空
let word = rest.match(wordRegExp)[1];
// 看word是不是全是空
if (!/^\s+$/.test(word)) {
// 不是全是空
// console.log('检测到文字', word);
// 改变此时stack2栈顶元素中
stack2[stack2.length - 1].children.push({ 'text': word, 'type': 3 });
}
// 指针移动标签的长度加3,为什么要加2呢?因为</>也占3位
index += word.length;
} else {
index++;
}
}
// 此时stack2就是我们之前默认放置的一项了,此时要返回这一项的children即可
return stack2[0].children[0];
};
- parseAttrsString.js
// 把attrsString变为数组返回:识别attrs;例如class,data-xx等
export default function (attrsString) {
if (attrsString == undefined) return [];
console.log(attrsString);
// 当前是否在引号内
var isYinhao = false
// 断点
var point = 0;
// 结果数组
var result = [];
// 遍历attrsString,而不是你想的用split()这种暴力方法
for (let i = 0; i < attrsString.length; i++) {
let char = attrsString[i];
if (char == '"') {
isYinhao = !isYinhao;
} else if (char == ' ' && !isYinhao) {
// 遇见了空格,并且不在引号中
console.log(i);
if (!/^\s*$/.test(attrsString.substring(point, i))) {
result.push(attrsString.substring(point, i).trim());
point = i;
}
}
}
// 循环结束之后,最后还剩一个属性k="v"
result.push(attrsString.substring(point).trim());
// 下面的代码功能是,将["k=v","k=v","k=v"]变为[{name:k, value:v}, {name:k, value:v}, {name:k,value:v}];
result = result.map(item => {
// 根据等号拆分
const o = item.match(/^(.+)="(.+)"$/);
return {
name: o[1],
value: o[2]
};
});
return result;
}