抽象语法树学习总结(简易)

169 阅读2分钟

vue中使用的template并不是真实的dom结构,只是vue的模板语法。所以需要将其转化为真实的dom。所以需要进行模板编译,模板编译的目标就是生成渲染函数。

模板编译成渲染函数需要两步:1.需要先将模板解析成AST抽象语法树。2.在使用AST生成渲染函数。

image.png

image.png

我们需要将第一张图片转为第二张图片。

解析器(将模板解析成AST抽象语法树)

export default function(tempalteStr){
    var index = 0 //定义指针
    var rest = tempalteStr //定义剩余字符串
    var stack1 = [] //检测开始标记时用到的栈
    var stack2 = [{children:[]}] //检测内容用到的栈;避免最后一项没有children进行添加
    // 开始标记
    var startRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/;
    // 结束标记
    var endRegExp = /^\<\/([a-z]+[1-6]?)\>/;
    // 抓取结束标记前的文字
    var wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/;
    while(index < tempalteStr.length-1){
        rest = tempalteStr.substring(index); //截取指针移动后剩余字符串
        if(startRegExp.test(rest)){ //检测是否为开始标记
            let tag = rest.match(startRegExp)[1] //获取标签名称
            let attrstr = rest.match(startRegExp)[2] //获取属性字符串
            stack1.push(tag) //栈1放入标签名
            stack2.push({ 'tag': tag, 'children': [],'attrs': parseAttr(attrstr)}) //栈2
             //因为<>占据两个字符,所以加2
            const attrsStrLength = attrstr!=null?attrstr.length:0
            index += tag.length + 2 + attrsStrLength //指针移动
        }else if(endRegExp.test(rest)){ //判断是否是结束标签
            let tag = rest.match(endRegExp)[1] //获取结束tag标签
            let pop_tag = stack1.pop() //获取结束标签后弹出栈1栈顶(也就是开始时的tag标签)
            if(tag == pop_tag){ //判断结束tag标签与开始tag标签是否匹配
                let pop_arr = stack2.pop() //匹配,弹出栈2栈顶
                if(stack2.length>0){ //弹出后栈2还有值,说明还有父节点标签
                    //将弹出的值放入父节点标签的children内
                    stack2[stack2.length-1].children.push(pop_arr)
                }
            }else{
                throw Error("该标签没有闭合")
            }
            //因为</>所以加3
            index += tag.length + 3; //移动指针
        }else if(wordRegExp.test(rest)){ //判断是不是文字
            let word = rest.match(wordRegExp)[1] //取出文字
            if(!/^\s+$/.test(word)){ //判断是不是标签内的文字
                console.log('检测到文字:'+word)
                stack2[stack2.length-1].children.push({text:word,type:3})
            }  
            index += word.length; //移动指针
        }else{
            index++ //移动指针
        }
    }
    return stack2[0].children[0]
}

主要使用指针和栈的思想。利用指针获取剩余的字符串。利用栈思想,只要匹配到开始标签,两个栈就同时进栈,只要匹配到结束标签与开始标签相匹配,就把栈2的最后一项内容弹出(此时最后一项就是上一项),塞到栈2的最后一项的children。

解析属性

// 把attrsString变为数组返回
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) {
            // 遇见了空格,并且不在引号中
            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;
}
例:'class="a b c" id="box"==>使用指针法,遇到"时,设置标记true,再次遇到"时设置标记为
false。然后寻找空格,只要找到的字符串为空格,且标记为false则代表此引号是隔开两个属性的端
点,将指针设置在这个位置。取出该段点前的字符串放入数组,以此类推。

使用AST生成代码字符串

这部分视频没有,乱写了一个!!!!
export default function render(ast) {
        console.log(ast)
        let attrObj = { attrs: '' }
        let withword
        if (ast.type) {
            return `_v(${ast.text})`
        }

        if (ast.attrs.length == 0 && ast.children.length == 0) {
            withword = `_c(${ast.tag})`
        } else if (ast.attrs.length > 0 && ast.children.length == 0) {
            let attrchObj = {}
            for (let i = 0; i < ast.attrs.length; i++) {
                attrchObj[ast.attrs[i].name] = ast.attrs[i].value
            }
            attrObj.attrs = attrchObj
            attrObj = JSON.stringify(attrObj)
            withword = `_c(${ast.tag},${attrObj})`
        } else if (ast.attrs.length == 0 && ast.children.length > 0) {
            let astch = []
            for (let i = 0; i < ast.children.length; i++) {
                astch.push(render1(ast.children[i]))
            }
            astch = JSON.stringify(astch)
            withword = `_c(${ast.tag},${astch})`
        } else if (ast.attrs.length > 0 && ast.children.length > 0) {
            let attrchObj = {}
            for (let i = 0; i < ast.attrs.length; i++) {
                attrchObj[ast.attrs[i].name] = ast.attrs[i].value
            }
            attrObj.attrs = attrchObj
            attrObj = JSON.stringify(attrObj)

            let astch = []
            for (let i = 0; i < ast.children.length; i++) {
                astch.push(render1(ast.children[i]))
            }
            astch = JSON.stringify(astch)
            withword = `_c(${ast.tag},${attrObj},${astch})`
        }
        return withword
}

这就生成了代码字符串,只要将内容放入渲染函数width(this)return{...}里面,该模板编译的任务就完成了。

总结

image.png

==》width(this)return{_c(div,{"attrs":{"class":"a b","id":"box"}},["_c(h3,["_v(kk)"])","_c(ul,["_c(li,["_v(1111)"])","_c(li,["_v(222)"])","_c(li,["_v(33)"])"])"])}

学习视频