AST 抽象语法树

675 阅读4分钟

代码地址

AST

简介

抽象语法树(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 []
    }
  }

优化

上述smartRepeatAST的实现我们为了方便都使用两个栈来实现,其实想vue底层都是使用一个栈实现的,AST的stackContent本身就有对应tag,在用tag时拿到即可,改动:stackTag[stackTag.length -1] => stackContent[stackContent.length -1].tag

至于smartRepeat可以使用stack存一个对象,包含num与str进行操作。