幡然醒悟篇之AST抽象语法树!

517 阅读3分钟

抽象语法树(AST)也是Vue的核心内容,也不仅仅限于Vue,如在Babel中的运用,在Vue中它也是虚拟节点的上一步,虚拟节点就是通过抽象语法树得来的,但是它很简单,因为它不需要去做diff,节点的diff是通过虚拟节点进行的!

dom更新的过程:

无标题-2022-08-04-1423.png

简写Vue抽象语法树, 抽象语法树我们手写的话也就两段代码: 1.扫描模板生成AST语法树 2.改变attrs结构

1.扫描模板生成AST语法树:

在写代码前,我们应该要了解栈的概念:什么是栈?先进去,后出来。

2.png

<div class='abc' id='efg' ref='hijk'> <ul> <li>苹果</li> <li>橘子</li> <li>菠萝</li> </ul></div>

3.png

指针扫描的思想在Vue中可以说是很多地方都用到了,如抽象语法树,虚拟dom的diff算法等。

这里是运用到了指针与正则,正则不会的话也没有关系,不影响生成抽象语法树的思路,毕竟我们这个也是一个最简版的!

我们需要两个栈 : stack1 = [] , stack2 = [{ children: [] }]

这里扫描模板时,会有3个判断:

example: <div id='123' class='456'>你好</div>

1.扫描到标签开始,如:<div id='123'> div入stack1栈 , 语法树:{'tag':'div','children':[],'attrs':{ id='123' class='456'} }入stack2栈

2.扫描到标签中间的内容,如:你好 ; (文本)stack2栈顶加入文本节点 {'text':'你好','type':3}

3.扫描到标签结束,如:</div> stack1栈顶出栈, stack2栈顶出栈并移到stack2新的栈顶的children中(出栈了它下一项就是新的栈顶了,这样就可以保证语法树的结构了)

import attrsParse from  './attrsParse.js'
export default function (htmlStr) {
    //指针
    let index = 0;
    //栈1 存放开始标签名称
    let stack1 = [];
    //栈2 存放标签中内容
    let stack2 = [{ children: [] }];
    //开始标签正则  例如:<div class='abc'  id='efg'>
    let startRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/;
    //结束标签正则  </div>
    let endRegExp = /^\<\/([a-z]+[1-6]?)\>/;
    //获取文字正则  文字是在 (文字)</...>    ^\([^\<]+) 不是<的文字
    let wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/;
   
    while (index < htmlStr.length -1 ) {
        //rest 剩余未遍历的字符
        let rest = htmlStr.substring(index);
        //匹配开始标记
        if (startRegExp.test(rest)) {
            //开始标记节点名称
            let tagName = rest.match(startRegExp);
            //入栈 例如:div
            stack1.push(tagName[1]);
            let attrs = tagName[2];
            //入栈  按照ast格式 attrsParse()解析attrs
            stack2.push({ 'tag': tagName[1], 'children': [], 'attrs': attrsParse(tagName[2]) });
            //<>占两个字符 + class 与 id 的长度
            let attrsLen = 0;
            if(attrs){
                attrsLen = attrs.length;
            }
            index = index + tagName[1].length + 2 + attrsLen;
        } else if (endRegExp.test(rest)) {
            //匹配结束标记 
            //出栈
            stack1.pop();
            let endTag = rest.match(endRegExp)[1];
            //出栈入新栈顶
            let stact2_pop = stack2.pop();
            stack2[stack2.length - 1].children.push(stact2_pop);
            // </>占三个字符 
            index = index + endTag.length + 3;
        }else if( wordRegExp.test(rest) ){
            //是文字
            let word = rest.match(wordRegExp)[1];
            // stack2 栈顶添加文字节点
            //如果word不全是空字符串
            if( /\S/g.test(word)){
                stack2[stack2.length - 1].children.push({'text':word,'type':3});  
            }
            index= index + word.length;
        }else{
            index++;
        }
    }
    //返回栈顶数据  只剩一项  直接stack2[0]也可
    return stack2[stack2.length - 1].children;
}


然后我们再改变下attrs的结构就行了: " id='123'| class='456'|" 这里也是扫描,扫到双数 ' 就截取到数组中。


export default function (attrs) {
   //  AST的attrs [{name:'',value:''}] 的形式  现在是这样的 " class='abc'  id='efg'  ref='hijk'"
   //继续使用扫描  
   if (attrs == undefined) return [];
   let point = 0;
   let isYinHao = true;
   //定义一个数组存放attrs
   let attrsList = [];
   for (let i = 0; i < attrs.length; i++) {
      //'abc' 第一个引号 false ,第二个就是 true 
      if (attrs[i] == "'") {
         isYinHao = !isYinHao;
      }
      //不是引号并且为 空字符时,就收集一次 
      if (isYinHao && attrs[i] == "'") {
         attrsList.push(attrs.substring(point, i + 1));
         //修改指针的位置为当前i+1  '的位置加1 下一个数据
         point = i + 1;
      }
   }

   //收集完成,将数据转换格式
   attrsList = attrsList.map(e => {
      e = e.trim();
      let item = {
         name: e.split('=')[0],
         value: e.split('=')[1].replace(/'/g,"")  //去掉引号,因为有两层 ""
      };
      return item;
   });
   return attrsList;
}

完事了,就两块代码,我们继续测试一下吧:

import parse from './parse.js';
let template = `<div class='abc'  id='efg' ref='hijk'>
        <ul>
            <li>
                苹果
            </li>
            <li>
                橘子
            </li>
            <li>
                菠萝
            </li>
        </ul>
    </div>`

const ast = parse(template);
console.log(ast);

打印的结果:

image.png

[
    {
        "tag": "div",
        "children": [
            {
                "tag": "ul",
                "children": [
                    {
                        "tag": "li",
                        "children": [
                            {
                                "text": "\n                苹果\n            ",
                                "type": 3
                            }
                        ],
                        "attrs": []
                    },
                    {
                        "tag": "li",
                        "children": [
                            {
                                "text": "\n                橘子\n            ",
                                "type": 3
                            }
                        ],
                        "attrs": []
                    },
                    {
                        "tag": "li",
                        "children": [
                            {
                                "text": "\n                菠萝\n            ",
                                "type": 3
                            }
                        ],
                        "attrs": []
                    }
                ],
                "attrs": []
            }
        ],
        "attrs": [
            {
                "name": "class",
                "value": "abc"
            },
            {
                "name": "id",
                "value": "efg"
            },
            {
                "name": "ref",
                "value": "hijk"
            }
        ]
    }
]