目录

一、概念介绍:

 二、抽象语法树与虚拟DOM节点的关系:

三、尝试手写AST语法树:

1. 识别开始结束标签(parse.js):

2. 使用栈形成AST(完善parse.js):

3.识别attrs:

parse.js:

parseAttrsString.js:


一、概念介绍:

在开发Vue的时候编译器会将模板语法编译成正常的HTML语法,而直接编译的时候是非常困难的,因此此时会借助AST抽象语法树进行周转,进而变为正常的HTML语法,使编译工作变得更加简单。

 


抽象语法树的本质上是一个JS对象,Vue在审视所有HTML结构时是以字符串的新式进行的,最终将其解析为JS对象。AST抽象语法树服务于模板编译,将一种语法翻译为另一种语法。在Vue中将模板语法编译为HTML语法,自己作为中转站。

 二、抽象语法树与虚拟DOM节点的关系:

图示:

抽象语法树的终点是渲染函数(h函数)。

渲染函数(h函数),它既是AST的产物,也是vnode(虚拟节点)的起源。h函数里面是不含指令的。

抽象语法树不会进行diff算法的并且抽象语法树不会直接生成虚拟节点,抽象语法树最终生成的是渲染函数的

三、尝试手写AST语法树:

注:项目实现环境 

webpack@5,webpack-cli@3,webpack-dev-server@3

1. 识别开始结束标签(parse.js):

基本思想:使用指针和栈的思想,使用正则表达式来匹配标签。

实现思路:

  • 有识别开始标签的正则表达式(识别a-z的小写字母和0-6数字组成的标签)
  • 识别结束符号的正则表达式(识别闭合的标签)
  • 识别文字的正则表达式(不考虑文字与标签同级的情况)
  • 在while循环中遍历传入的字符串
  • 定义两个栈,栈1和栈2。

当识别到一个开始的标签符号,那么就将这个标签入栈1,把空字符串入栈2。

当识别到的字符是标签内的文字,那么就将栈2栈顶这项改为文字。

当识别到结束标签,那么就将这个标签符号弹栈,就把栈2栈顶的元素push到栈二新的栈顶上的。

export default function(templateString){
    //准备指针
    var index = 0;
    //剩余部分
    var rest = '';
    //开始标记
    var startRegExp = /^\<([a-z]+[1-6]?)\>/;
    //结束标签
    var endRegExp =  /^\<\/([a-z]+[1-6]?)\>/;
    //识别文字
    var wordRegExp = /^([^\<]+)\<\/([a-z]+[1-6]?)\>/;
    //准备两个栈
    var stack1 = [];
    var stack2= [];
    //遍历
    while(index<templateString.length-1){
        rest = templateString.substring(index);
        //识别遍历到的这个字符,是不是一个开始标签
        if(startRegExp.test(rest)){
            console.log(rest.match(startRegExp));
            let tag = rest.match(startRegExp)[1];
            console.log("检测到开始标记",tag);

            //将开始标记推入栈1中
            stack1.push(tag);
            //将空数组推入栈2 中
            stack2.push([]);

            //指针移动标签的长度+2?因为<>也占两位
            index += tag.length+2;
        }else if(endRegExp.test(rest)){
            //识别这个字符是不是一个结束标签
            //指针移动标签的长度加3,</>占三位
            let tag = rest.match(endRegExp)[1];
            console.log("检测到结束标记",tag);
            //此时tag一定是和栈1顶部的是相同的

            if(tag == stack1[stack1.length-1]){
            //    弹栈
                stack1.pop();
            }else{
                throw new Error(stack1[stack1.length-1]+"标签没有封闭");
            }
            index+=tag.length+3;
            console.log(stack1);
            console.log(stack2);
        }else if(wordRegExp.test(rest)){
        //    遍历到这个字符是不是文字
            let word = rest.match(wordRegExp)[1];
            if(!/^\s+$/.test(word)){
            //    不是全是空
                console.log("检测到文字",word);
            }

            index+=word.length;
        } else{
            index++;
        //    标签中的文字
        }

    }
    return templateString;
}

2. 使用栈形成AST(完善parse.js):

当识别到一个开始的标签符号,那么就将这个标签入栈1,把空字符串入栈2。

当识别到的字符是标签内的文字,那么就将栈2栈顶这项改为文字。

当识别到结束标签,那么就将这个标签符号弹栈,就把栈2栈顶的元素push到栈二新的栈顶上的。

export default function(templateString){
    //准备指针
    var index = 0;
    //剩余部分
    var rest = '';
    //开始标记
    var startRegExp = /^\<([a-z]+[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);
        //识别遍历到的这个字符,是不是一个开始标签
        if(startRegExp.test(rest)){
            let tag = rest.match(startRegExp)[1];
            console.log("检测到开始标记",tag);

            //将开始标记推入栈1中
            stack1.push(tag);
            //将空数组推入栈2 中
            stack2.push({'tag':tag,'children':[]});

            //指针移动标签的长度+2?因为<>也占两位
            index += tag.length+2;
        }else if(endRegExp.test(rest)){
            //识别这个字符是不是一个结束标签
            //指针移动标签的长度加3,</>占三位
            let tag = rest.match(endRegExp)[1];
            console.log("检测到结束标记",tag);
            //此时tag一定是和栈1顶部的是相同的
            let pop_tag = stack1.pop();
            if(tag == pop_tag){
            //    弹栈
                let pop_arr = stack2.pop();
                console.log(pop_arr);
                if(stack2.length>0){
                    stack2[stack2.length-1].children.push(pop_arr);
                }
            }else{
                throw new Error(stack1[stack1.length-1]+"标签没有封闭");
            }
            index+=tag.length+3;
            console.log(stack1);
            console.log(stack2);
        }else if(wordRegExp.test(rest)){
        //    遍历到这个字符是不是文字
            let word = rest.match(wordRegExp)[1];
            if(!/^\s+$/.test(word)){
            //    不是全是空
                console.log("检测到文字",word);
            //    改变此时Stack2栈顶元素中
                stack2[stack2.length-1].children.push({'text':word,'type':3});
            }

            index+=word.length;
        } else{
            index++;
        //    标签中的文字
        }

    }
//    此时stack2就是我们之前默认放置的一项了,此时要返回这一项的children即可
    return stack2[0].children[0];
}

3.识别attrs:

  • 首先需要我们更改startRegExp的正则表达式,使其能够识别我们添加的attrs。
  • 指针要移动长度因为添加的attrs会发生改变。
  • 因为获取到的attrsString的长度可能为undefined,所以采用三元运算法给其添加赋值。
  • 当前这个标签要有attrs这个属性,所以往栈二push的时候要添加上attrs属性。
  • 我们通过正则获取到的attrsString是字符串但此时我们需要的是对象的形式,所以我们需要建一个parseAttrsString.js来处理我们获取到的attrsString的字符串

parse.js:

import parseAttrsString from './parseAttrsString.js'

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);
        //识别遍历到的这个字符,是不是一个开始标签
        if(startRegExp.test(rest)){

            let tag = rest.match(startRegExp)[1];
            let attrsString = rest.match(startRegExp)[2];

            const attrsStringLength = attrsString != null ? attrsString.length : 0;

            console.log("检测到开始标记",tag);

            //将开始标记推入栈1中
            stack1.push(tag);
            //将空数组推入栈2 中
            stack2.push({'tag':tag,'children':[],'attrs':parseAttrsString(attrsString)});

            //指针移动标签的长度+2?因为<>也占两位
            index += tag.length+2+attrsStringLength;
        }else if(endRegExp.test(rest)){
            //识别这个字符是不是一个结束标签
            //指针移动标签的长度加3,</>占三位
            let tag = rest.match(endRegExp)[1];
            console.log("检测到结束标记",tag);
            //此时tag一定是和栈1顶部的是相同的
            let pop_tag = stack1.pop();
            if(tag == pop_tag){
            //    弹栈
                let pop_arr = stack2.pop();
                console.log(pop_arr);
                if(stack2.length>0){
                    stack2[stack2.length-1].children.push(pop_arr);
                }
            }else{
                throw new Error(stack1[stack1.length-1]+"标签没有封闭");
            }
            index+=tag.length+3;
            console.log(stack1);
            console.log(stack2);
        }else if(wordRegExp.test(rest)){
        //    遍历到这个字符是不是文字
            let word = rest.match(wordRegExp)[1];
            if(!/^\s+$/.test(word)){
            //    不是全是空
                console.log("检测到文字",word);
            //    改变此时Stack2栈顶元素中
                stack2[stack2.length-1].children.push({'text':word,'type':3});
            }

            index+=word.length;
        } else{
            index++;
        //    标签中的文字
        }

    }
//    此时stack2就是我们之前默认放置的一项了,此时要返回这一项的children即可
    return stack2[0].children[0];
}

 

parseAttrsString.js:

函数功能:将获取到的字符串attrs转换成数组对象的形式。

思路:利用引号和空格将其拆分。

  • 首先遍历字符串,遇到引号后将其 isStr 属性设置成true,此时在引号内遇到空格不用管,当遇到下一个引号时设置 isStr 为false。此时在 isStr为false的情况下再遇见引号就将前面的字符串截取出来放入到数组中。
  • 最后将数组里面的内容利用map进行拆分。
//把attrsString变为数组返回
export default function(attrsString){
    if(attrsString == undefined) return [];
//    当前是否在引号内
    var isStr = false;
//    断点
    var point = 0;
//    结果数组
    var result = [];
//    遍历attrsString
    for(let i = 0;i<attrsString;i++){
        let char = attrsString[i];
        if(char == '"' ){
            isStr = !isStr;
        }else if(char == ' ' && !isStr){
        //    遇见了空格,并且不在引号内
            console.log(i);
            if(!/^\s*$/.test(attrsString.substring(point,i))){
                result.push(attrsString.substring(point,i).trim());
                point = i;
            }

        }
    }
//    循环结束之后,最后还剩一个属性
    result.push(attrsString.substring(point).trim());

    result = result.map(item=>{
    //    根据等号拆分
        const o = item.match(/^(.+)="(.+)$/);
        return{
            name:o[1],
            name:o[2]
        }
    });
    return result;
}

注:学习资料《尚硅谷Vue源码系列课程》

代码地址

Logo

前往低代码交流专区

更多推荐