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

101 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

喜欢就点个赞吧🎈😊

🎉 前言

AST抽象语法树是指对模板语法进行整理,这样能够是的编译的工作变得简单,抽象语法树是采用JS的对象来描述的,抽象语法树生成的就是h渲染函数,这是生成虚拟节点之前的一个步骤,用来进行模板编译。

image.png

🎉 一道有用的算法题

想要了解AST的核心思想需要从一道算法题进行入手,这是AST语法的核心思想。 接下来我们看题

image.png

先写一下本体的思路:首先需要使用的思想就是栈和指针。

  1. 我们需要指针遍历每一个字符,如果字符是数字和[的结合,那么就要将数字压栈1,并且在另一个栈2中压入空的字符串。
  2. 然后如果字符是字母,旧要将栈2顶的项赋值为这个字母。
  3. 如果字符为]那么就要将栈1和栈2的栈顶元素进行弹出,并且将栈2中的字符赋值给栈2的栈顶元素。

image.png

 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]);
        }

🎉 AST的思路

image.png

// parse函数,主函数
export default function(templateString) {
    // 指针
    var index = 0;
    // 剩余部分
    var rest = '';
    // 开始标记
    var startRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/;
    // 结束标记  [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);
        // 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];
};

🎉 拓展知识

在使用字符匹配时要使用到正则的相关知识,可以来我的掘金看一看干货: juejin.cn/post/713133…

🎉 总结

对于AST来说,理解核心思想是非常重要的,可以将其看成一道算法题来进行试炼。