代码地址
简介
抽象语法树(Abstract Syntax Tree)本质上是一个 js 对象,用来描述模版语法
graph TD
模版 --> 抽象语法树 --> h函数 --> 虚拟节点 --> 界面
相关算法题
寻找字符串中连续重复次数最多的字符
// 寻找字符串中连续重复次数最多的字符
const str = 'aaaaaaaabbbbbbbbbbbbbbbbbcccccccccdddddd'
function getRepeat(str){
if(!str) return
let maxTimes = 0,
maxChar = str[0],
start = 0,
end = 0
while(start < str.length) {
// 不相等,说明字符不连续了,
if(str[start] != str[end]){
if(end - start > maxTimes) {
maxTimes = end - start
maxChar = str[start]
}
start = end
}
// 相等,继续移动指针
end++
}
return {
maxTimes,
maxChar
}
}
console.log(getRepeat(str))//{maxTimes: 17, maxChar: 'b'}
用递归的方法输出斐波那契数列前 10 项
// 2,用递归的方法输出斐波那契数列前 10 项
function fib(n) {
// 缓存数据,避免重复计算
const cache = {}
function fn(index){
if(cache[index]) return cache[index]
else {
cache[index] = (index == 1 || index == 0) ? 1 : fn(index-1) + fn(index-2)
return cache[index]
}
}
for (let index = 0; index < n; index++) {
console.log(fn(index))
}
}
fib(10)
形式转换
递归
将数组 [1, 2, 3, [4, 5, [6, 7]], 8] 转为下图所示的对象格式
{
"children":
[
{ "value": 1 },
{ "value": 2 },
{ "value": 3 },
{
"children":
[
{ "value": 4 },
{ "value": 5 },
{
"children":
[
{ "value": 6 },
{ "value": 7 }
]
}
]
},
{ "value": 8 }
]
}
function convert(item) {
if (typeof item === 'number') {
return {
value: item
}
}
if (Array.isArray(item)) {
return {
children: item.map(val => convert(val))
}
}
}
let item = [1, 2, 3, [4, 5, [6, 7]], 8]
console.log(JSON.stringify(convert(item)))
栈
将字符串 3[1[a]2[b]] 转换成 abbabbabb
这里就用到栈的思想,准备两个栈,一个存放数字,一个存放临时字符串,用一个指针遍历 3[1[a]2[b]],
- 当指针指向的为数字时,就把数字压入数字栈中
- 当指针指向的为
[时,就把一个空字符串压入字符串栈中 - 当指针指向的为字母时,就把字符串栈中栈顶的这一项改为这个字母
- 当指针指向的为
]时,就把数字弹栈,字符串中栈顶的这项重复刚刚这个弹出的数字次数,弹栈,然后拼接到新栈顶
function smartRepeat(str){
let i = 0,
// 剩余的字符串
resStr = str,
// 存放数字的栈
stackNum = [],
// 存放字符串的栈
stackStr = [],
// 数字+[
regExpStsrt = /^(\d+)\[/,
// 字母 + ]
regExpEnd = /^(\w+)\]/;
while(i < str.length - 1){
// 最新的剩余字符串
resStr = str.substring(i)
// 数字连着【开头
if(regExpStsrt.test(resStr)) {
let num = resStr.match(regExpStsrt)[1]
// 对应数字入栈(每个数字对应的字符串也存起来)
stackNum.push(num)
stackStr.push('')
// 移动指针,直接移过【
i += num.length + 1
}else if(regExpEnd.test(resStr)) {
const str = resStr.match(regExpEnd)[1]
// 将字符串栈的栈顶的那一项赋值为捕获的字母
stackStr[stackStr.length - 1] = str
// 直接跳过字母的长度
i += str.length
}else if(resStr[0] == ']') {
// 对应数字出栈
const popNum = stackNum.pop()
const popStr = stackStr.pop()
// 字符串拼接
stackStr[stackStr.length - 1] += popStr.repeat(popNum)
i++
}
}
return stackStr[0].repeat(stackNum[0])
}
console.log(smartRepeat('2[2[cwa]1[d]]'))
AST
平时在 .vue 文件里写在 template 里的看似 dom 的内容,事实上会经由 vue-loader 的解析,作为字符串提取处理。实现 AST 的原理根本上就是把一段字符串通过指针逐个遍历,根据不同情况进行不同的处理
AST转换格式
模版
<div>
<h3 id="legend" class="jay song">范特西</h3>
<ul>
<li>七里香</li>
</ul>
</div>`
AST
{
"tag": "div",
"attrs": [],
"type": 1,
"children": [
{
"tag": "h3",
"attrs": [
{
"name": "id",
"value": "legend"
},
{
"name": "class",
"value": "jay song"
}
],
"type": 1,
"children": [
{
"text": "范特西",
"type": 3
}
]
},
{
"tag": "ul",
"attrs": [],
"type": 1,
"children": [
{
"tag": "li",
"attrs": [],
"type": 1,
"children": [
{
"text": "七里香",
"type": 3
}
]
}
]
}
]
}
转换思路
我们可以准备两个栈和一个用于遍历模板字符串的指针:
- 指针遇到标签则往一个栈(标签栈)中加入该标签名,另一个栈(数组栈)中加入一个空数组(代码里为了方便事实上是加入一个对象
{ children: [] }) - 指针遇到文字则将数组栈中的栈顶的数组内容改为文字
- 指针遇到闭合标签则将标签栈和数组栈都进行出栈操作(数组栈出栈的内容就是标签栈出栈的标签的内容),然后将出栈的这两个元素组合下,拼接到数组栈的新栈顶的那个数组里。
正则注意事项:
用法一: 限定开头
文档上给出了解释是匹配输入的开始,如果多行标示被设置成了true,同时会匹配后面紧跟的字符。 比如 /^A/会匹配"An e"中的A,但是不会匹配"ab A"中的A
用法二:(否)取反
当这个字符出现在一个字符集合模式的第一个字符时,他将会有不同的含义。
比如: /[^a-z\s]/会匹配"my 3 sisters"中的"3" 这里的”^”的意思是字符类的否定,上面的正则表达式的意思是匹配不是(a到z和空白字符)的字符。
AST代码
//处理属性的方法
import parseAttrs from './parseAttrs.js'
function parse(templateStr) {
// 准备一个指针
let i = 0
// 准备两个栈
// 初始添加元素 { children: [] } 是因为如果不加, stackContent 在遇到最后一个封闭标签进行弹栈后,stackContent 里就没有元素了,也没有 .children 可以去 push 了
const stackTag = [], stackContent = [{ children: [] }]
// 指针所指位置为开头的剩余字符串
let restTemplateStr = templateStr
// 识别开始标签的正则
const regExpStart = /^<([a-z]+[1-6]?)(\s?[^>]*)>/
while (i < templateStr.length - 1) {
restTemplateStr = templateStr.substring(i)
// 遇到开始标签
if (regExpStart.test(restTemplateStr)) {
const startTag = restTemplateStr.match(regExpStart)[1] // 标签
const attrsStr = restTemplateStr.match(regExpStart)[2] // 属性
// 标签栈进行压栈
stackTag.push(startTag)
// 内容栈进行压栈
stackContent.push({
tag: startTag,
attrs: parseAttrs(attrsStr),
type: 1,
children: []
})
i += startTag.length + attrsStr.length + 2 // +2 是因为还要算上 < 和 >
} else if (/^<\/[a-z]+[1-6]?>/.test(restTemplateStr)) { // 遇到结束标签
const endTag = restTemplateStr.match(/^<\/([a-z]+[1-6]?)>/)[1]
// 结束标签应该与标签栈的栈顶标签一致
if (endTag === stackTag[stackTag.length -1]) {
// 两个栈都进行弹栈
stackTag.pop()
const popContent = stackContent.pop()
stackContent[stackContent.length - 1].children.push(popContent)
i += endTag.length + 3 // +3 是因为还要算上 </ 和 >
} else {
throw Error('标签' + stackTag[stackTag.length -1] + '没有闭合')
}
} else if (/^[^<]+<\/[a-z]+[1-6]?>/.test(restTemplateStr)) { // 遇到内容
const wordStr = restTemplateStr.match(/^([^<]+)<\/[a-z]+[1-6]?>/)[1] // 捕获结束标签 </> 之前的内容,并且不能包括开始标签 <>
if (!/^\s+$/.test(wordStr)) { // 如果捕获的内容不为空
// 将内容栈栈顶元素进行赋值
stackContent[stackContent.length - 1].children.push({
text: wordStr,
type: 3
})
}
i += wordStr.length
} else {
i++
}
}
// 因为定义 stackContent 的时候就默认添加了一项元素 { children: [] },现在只要返回 children 的第一项就行
return stackContent[0].children[0]
}
const templateStr = `<div>
<h3 id="legend" class="jay song">范特西</h3>
<ul>
<li>七里香</li>
</ul>
</div>`
const ast = parse(templateStr)
console.log(JSON.stringify(ast))
export default function(attrsStr) {
const attrsStrTrim = attrsStr.trim() // 去空格
if (attrsStrTrim) {
let point = 0 // 断点
let isYinhao = false // 是否是引号
let result = [] // 结果数组
for (let index = 0; index < attrsStrTrim.length; index++) {
if (attrsStrTrim[index] === '"') isYinhao = !isYinhao
// 遇到空格且不在双引号内,就截取从 point 到此的字符串(属性分割)
if (!isYinhao && /\s/.test(attrsStrTrim[index])) {
const attrs = attrsStrTrim.substring(point, index)
result.push(attrs)
point = index
}
}
result.push(attrsStrTrim.substring(point + 1)) // 最后一个属性是没有通过 for 循环得到的,所以要专门加上,+1 是为了去除开始的空格
// ["id="legend"", "class`="`jay song""]
result = result.map(item => {
// 根据等号拆分
const itemMatch = item.match(/(.+)="(.+)"/)
return {
name: itemMatch[1],
value: itemMatch[2]
}
})
return result
} else {
return []
}
}
优化
上述smartRepeat与AST的实现我们为了方便都使用两个栈来实现,其实想vue底层都是使用一个栈实现的,AST的stackContent本身就有对应tag,在用tag时拿到即可,改动:stackTag[stackTag.length -1] => stackContent[stackContent.length -1].tag;
至于smartRepeat可以使用stack存一个对象,包含num与str进行操作。