Vue源码探析之AST抽象语法树

一、前言算法讲解:

1.指针思想

        问题:找一段字符串中,连续且重复次数最多的字符

ps:如果多个,只需要找到第一个即可,如a,b都是连续最多,先找到的如果是a那就是a。

// 指针思想,提升遍历效率,每个字符只遍历一遍

var str = 'abcdaabbddccccwssdwdaddwwjjjjjjjjxxxxdd'

var i = 0
var j = 0

var maxRepeatChar = ''

var maxReapeatCount = 0

console.log(str)
while(i <= str.length - 1){
    if (str[i] != str[j]){
        // console.log('找到目前最长得'+str[i]+'重复了'+(j-i))
        if(j-i > maxReapeatCount){
            maxReapeatCount = j-i;
            maxRepeatChar = str[i]
        }
        i = j
    }
    j++
}
console.log("结果连续字符最长的是",maxRepeatChar,'数量是',maxReapeatCount)

总结:

        这道算法题旨在考验对指针的掌握,如果是简单循环查找计算同样也可以完成,但是需要走很多重复的步骤,浪费了算力,通过i,j指针的使用,可以使得一次遍历,无需重复,这就是指针的重要作用,在这里就是下标法。(注意这里是指针指的是指针思想,其实在这更像下标法,但是思想是指针思想,不像C语言那样改内存啥的,这里就是下标)

2.斐波那契数列:

        问题:求得第n项斐波那契数列数列,,斐波那契就不多阐述了,就是典中典的递归运用,但是会面临一个问题,那就是重复计算,比如:求解fib(9)的时候会算fib(8)和fib(7),但是算fib(8)的时候又去算fib(7)和fib(6),这样fib(7)就进行了重复计算

// 递归经典,斐波那契数列

function fib(n){
    return n==0 || n==1 ? 1: fib(n-1) + fib(n-2)
}

for (let i =0;i<=9;i++){
    console.log(fib(i))
}

2.1斐波那契+缓存思想

        背景:2代码处斐波那契数列虽然实现没问题,但是面临一个大量重复计算的问题,这也是对算力的一种浪费,所以引入缓存思想,在计算一个fib(n)之前先去缓存中查找,如果已经算过了直接取值即可,无需再做计算,这样大大节省了性能

        

// 递归经典,斐波那契数列,解决重复计算问题,

// 创建缓存对象
var cache = {}

function fib(n){
    // 判断缓存对象中有没有这个值,如果有直接用
    if(cache.hasOwnProperty(n)){
        // 如果有这里直接return结果,代码就不会走往下的逻辑了
        return cache[n]
    }
    // 缓存对象没有值
    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))
    console.log(cache)
}

3.递归例题2(如果出现了规则的复现就要想到递归)

        问题:试将高维数组[1,2,[3,[4,5],6],7,[8],9]变为图中所示的对象

// 测试数组
var arr = [1,2,[3,[4,5],6],7,[8],9]

// 转换函数
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
}

写法二:

        利用map映射的方法,为什么会想到映射,其实针对arr测试数组来说,他其实可以说只有6项,分别1,2,【3,【4,5】,6】,7,【8】,9,对于这【3,【4,5】,6】项就可以看出映射关系 对于arr来说其实他就是一个chilren,里面的内容是映射内容。

        PS:写法一的递归次数远小于写法二,因为写法一只有数组递归,但是写法二是什么都递归一下item.map(_item => convert(_item))在遍历的时候递归了每一项

// 写法二
// 参数不是arr这个词语了,item可能是数组,也可能是数字
function convert(item){
    if(typeof(item) == 'number'){
        return {
            value: item
        }
    } else if(Array.isArray(item)) {
        // 如果传进来的参数是数组
        return {
            children:item.map(_item => convert(_item))
        }
    }
}

4.栈

        栈又名堆栈,它是一种运算受限的线性表,仅在表尾能进行插入和删除操作。这一端被称为栈顶,相对地,把另一端称为栈底

        向一个栈插入新元素又称作进栈、入栈或压栈;从一个栈删除元素又被称作出栈或退栈

        后进后出(LIFO)的特点:栈中的元素,最先进栈的必定是最后出栈,后进栈的一定会先出栈。

        javaScript中,栈可以用数组模拟。需要限制只能使用push()和pop(),不能使用unshift()和shift()。即,数组尾就是栈顶

        当然,可以用面向对象等手段,将栈封装的更好

4.1栈题目

        解题思路:

①词法分析的时候,经常要用到栈这个数据结构

②初学者大坑:栈的题目和递归非常像

        试编写"智能重复" smartRepeat函数,实现:

        将3[abc]变为abcabcabc
        将3[2[a]2[b]]变为aabbaabbaabb
        将2[1[a]3[b]2[3[c]4[d]]]变为abbbcccddddcccddddabbbcccddddcccdddd
        不用考虑输入字符串是非法的情况,比如:
         2[a3[b]]是错误的,应该补一个1,即2[1[a]3[b]]

          [abc]是错误的,应该补一个1,即1[abc]

前置知识:

 

 代码部分:

function smartRepeat(templateStr){
    // 指针
    var index = 0
    // 两个栈,存放数字
    // 栈1,存放数字
    var stack1 = []
    // 栈2,存放临时字符串
    var stack2 = []
    // 剩余部分
    var rest = templateStr

    while(index < templateStr.length - 1){
        // 剩余部分
        rest = templateStr.substring(index)
        // console.log(templateStr[index],rest)

        // 看当前剩余部分是不是以数字和[开头
        if (/^\d+\[/.test(rest)){
            // console.log("以数字开头")
            // 得到这个数字
            let times = Number(rest.match(/^(\d+)\[/)[1])
            // 如果这个是数字就压栈,空字符串也要压栈
            stack1.push(times)
            stack2.push('')

             // 让指针后移,times这个数字是多少位就后移多少位数+1,+1的是左方括号
             index += times.toString().length + 1
        }else if(/^\w+\]/.test(rest)){
            // 如果这个字符是字母,那么此时就把栈顶这项改为这个字母
            // 这里为什么要加[1]因为match返回的是一个数组,下标为1的才是需要项
            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的方法
            stack2[stack2.length - 1] += word.repeat(times)


            index++
        }
        console.log(index,stack1,stack2)
    }

    // while结束之后,stack1和stack2中肯定还有一项,返回他们的相乘即可,如果组成的字符串不对
    // 或者个数不对,可能是用户的问题,方括号没有闭合之类的
    return stack2[0].repeat(stack1[0])
}

console.log(smartRepeat('3[2[a]2[b]]'))

二、AST抽象语法树实现:

1.index.js

import parse from "./parse"

var templateString = `
        <div>
            <h3 class="23 aa cc" id="66" data-n="8">你好</h3>
            <ul>
                <li>A</li>
                <li>B</li>
                <li>C</li>
            </ul>
        </div>
`
const ast = parse(templateString)

console.log(ast)

2.parse.js

import parseAttrsString from "./parseAttrsString"
// parse函数 主函数
export default function (templateString){
    // 指针
    var index = 0
    // 剩余部分
    var rest = ''
    // 开始标记
    // var statRegExp = /^\<([a-z]+[1-6]?)\>/
    // 当有attrs属性的时候需要修改规则
    var statRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/
    // 结束标记
    var endRegExp = /^\<\/([a-z]+[1-6]?)\>/
    // 检测到文本
    var wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/
    // 准备两个栈
    var stack1 = []
    // 补充默认项children,这样就无需判断栈是否为空了
    var stack2 = [{'children':[]}]

    while(index < templateString.length -1) {
        rest = templateString.substring(index)
        // console.log("han",rest)
        // 识别遍历到的这个字符,是不是一个开始标签
        if (statRegExp.test(rest)){
                let tag = rest.match(statRegExp)[1]
                let attrsString = rest.match(statRegExp)[2]
                console.log(attrsString,666)
                console.log("检测到开始标记",tag)
                // 讲开始标记推入栈中
                stack1.push(tag)
                // 讲空数组推入栈2
                stack2.push({'tag':tag,'children':[],'attrs':parseAttrsString(attrsString)})
                // console.log(stack1,stack2,'查看入栈情况')
                // 得到attrs的总长度
                const attrsStringLength = attrsString != null ? attrsString.length : 0

                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是不是和入栈的时候标签相同?,一定是相同的
            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(stack1[stack1.length -1]+'标签没用闭合!')
            }
            // 指针移动标签的长度加3,为什么加3因为</>也站3位
            index += tag.length +3
            // console.log(stack1,stack2,'查看出栈情况')
        }else if(wordRegExp.test(rest)){
            let word = rest.match(wordRegExp)[1]
            // 看看word是不是全是空
            if (!/^\s+$/.test(word)){
                console.log('监测到文字',word)
                stack2[stack2.length - 1].children.push({'text':word,'type':3})
            }
            index += word.length
        }
        else{
            index ++
        }
        
    }
    console.log(stack2)
    // 此时stack2就是我们之前默认放置的一项了,此时要返回的这一项的children即可
    return stack2[0].children[0]
}

3.parseAttrsString.js

export default function(attrsString){
    if (attrsString == undefined) return []

    // 当前是否在引号内
    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){
            // 遇到空格,并且不在引号内
            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]
        }
    })
    
    console.log(result)
    return 66
}

实现思路及方法:

        利于了指针思想和栈的数据结构相结合,栈的数据结构在词法分析中常常用到,指针的作用是便于遍历,AST语法树的遍历采用了指针移动的方法,指针移动的优势是减少遍历长度,不需要每次都从头遍历,如果已经处理过的部分只需要指针前移,也就是index+=?(具体移动的步骤),然后这里需要处理的就是剩余部分用rest变量代表,我们处理的字符串就是DOM结构字符串类型,所以需要用到两个栈做存储,简单命名stack1和stack2,这里为什么需要两个栈呢?因为stack1主要是复杂出入栈的操作,因为DOM结构字符串除了开始标记还有结束标记,并且有拥有复杂嵌套关系,所以结合栈的结构类型后非常贴合,因为越内部的HTML标签他出栈也是最近的,例如最内层的是<li>标签那么最先遇到的一定是</li>,因为多部份html标签都是成对出现的结合栈就能完成标签的出入栈操作,stack1主要作用就是负责使得标签按照既定顺序出入栈,而stack2中存储的是他们出入栈的时候需要操作的数据,例如最外层<div>标签有tag和children等属性,那么在<div>标签入栈的时候会将队友属性push到stack2中,那么</div>做出栈操作的时候就能很方便的找到对应的操作数据了,这是实现的思路和4.1智能排序的算法题是大致相同的,不同的就是通过正则表达式筛选出符合条件的字符串部分并且处理了,例如结束的标签如下,

var endRegExp = /^\<\/([a-z]+[1-6]?)\>/

只需要在遇到开始标签的时候执行stack1入栈div标签和stack2入栈标签携带的属性,出栈的时候stack1进行pop操作,同时stack2处通过操作当前数据并且将整合好的数据push到stack2的上一层中,那么依次类推当到最外侧div的时候,stack2数组中已经将内部所以其他元素数据操作都完成了那么最终stack2中存储的就是从最内部一层一层处理完成后返回的数据,也就是我们最终需要的数据结构,如图:

参考视频:

【尚硅谷】Vue源码解析之AST抽象语法树

部分图片来源:

【尚硅谷】Vue源码解析之AST抽象语法树

猜你喜欢

转载自blog.csdn.net/weixin_54515240/article/details/130261374