四、AST抽象语法树

抽象语法树是什么

抽象语法树本质上就是一个js对象,大概原理就是代码字符串一个个字符的分析从而生成AST;例如mustache模板引擎中的tokens就是AST和vnode的简化版

1.png

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
image.png
        // 试输出斐波那契数列的前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.png

image.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;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容