手写Vue2: 实现模版转换成ast语法树

279 阅读2分钟

HTML模版

<div id="app" style="color:beige">
    <div style="color:red">{{name}}</div>
    <div>{{age}}</div>
</div>

转换成ast语法树

代码


// 正则表达式系列

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;  
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 >
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g


// ✅定义一些变量
let root = null; // ast语法树的树根
let currentParent; // 标识当前父亲是谁,指向栈顶元素
let stack = [];
const ELEMENT_TYPE = 1;
const TEXT_TYPE = 3;

// 创建一个虚拟的AST节点
function createASTElement(tagName, attrs) {
    return {
        tag: tagName,
        type: 1,
        children: [],
        attrs,
        parent: null
    }
}

// ✅ 需要转换成一颗抽象语法树
/**
 * 使用一个栈模拟(原理:栈括号配对算法题)
 */
function start(tagName, attrs) {
    // 遇到开始标签 就创建一个ast元素s
    let element = createASTElement(tagName,attrs);
    if(!root){
        root = element;
    }
    currentParent = element; // 把当前元素标记成父ast树
    stack.push(element); // 将开始标签存放到✅栈中
}

function chars(text) { // 将当前文本放到 父ast树的子节点
    text = text.replace(/\s/g,'');
    if(text){
        currentParent.children.push({
            text,
            type:3
        })
    }
}

function end(tagName) {
    let element = stack.pop(); // 拿到的是ast对象
    // 我要标识当前这个p是属于这个div的儿子的
    currentParent = stack[stack.length-1]; // 指向 剩下标签的 栈顶
    if(currentParent){
        // 双向赋值的保存
        element.parent = currentParent; // </div>: 先入栈的为
        currentParent.children.push(element); // 实现了一个树的父子关系
    }
}

// 函数入口
export function compileToFunction(template) {
    // console.log(template);

    let ast = parseHTML(template);

    // 1) 解析html字符串 将html字符串 => ast语法树

    // 需要将ast语法树生成最终的render函数  就是字符串拼接 (模板引擎)
}

function parseHTML(html) { //  <div>hello</div>
    // debugger;
    while (html) {
        // debugger;
        let textEnd = html.indexOf('<');
        if (textEnd == 0) { // 是一个开始标签或者结束标签
            // 如果当前索引为0 肯定是一个标签 开始标签 结束标签
            let startTagMatch = parseStartag(); // 通过这个方法获取到匹配的结果 tagName,attrs
            // console.log(startTagMatch);
            if (startTagMatch) { //这里说明开始标签已经有匹配了
                // 处理获取的字符
                start(startTagMatch.tagName, startTagMatch.attrs); // 1解析开始标签
                continue; // 如果开始标签匹配完毕后 继续下一次 匹配
            }
            // 不是开始标签的话 就执行这个 startTagMatch为null
            let endTagMatch = html.match(endTag);
            if (endTagMatch) { //匹配结束标签
                advance(endTagMatch[0].length);
                // 处理获取的字符
                end(endTagMatch[1]); // 2解析结束标签
                continue;
            }
        }
        let text; //文本标签
        if (textEnd >= 0) {// 说明是文本结束位置
            text = html.substring(0, textEnd);
        }
        if (text) {
            advance(text.length);
            // 处理获取的字符
            chars(text); // 3解析文本
        }
    }

    console.log('------------');
    console.log(root);

    function parseStartag() {
        /**
         *  拿到开始标签, 以<div 为例: 
         * start[0] = '<div'
         * start[1] = 'div'
         */
        let start = html.match(startTagOpen);
        if(start) { 
            const match = { //将开始标签 组成一个对象
                tagName:start[1], //'div'
                attrs:[]
            }
            advance(start[0].length) //'<div'

            // console.log(start); 
            // console.log(html); 
            // console.log('-----以上为截取 <div -------'); 

            let startToEnd; // 开始标签的 > 符号,注意,这里每次的定义 html.match(startTagClose)) 不能写在外面
            let startAttribute; // 匹配开始标签里面的属性
            // console.log(startToEnd);
            // console.log(startAttribute);
            while(!(startToEnd = html.match(startTagClose)) && (startAttribute = html.match(attribute))) {
                advance(startAttribute[0].length) //删除匹配的字符(属性)
                // console.log(startAttribute); //  

                // 核心
                match.attrs.push({ //将 id='app' 存到match的attr数组中
                    name: startAttribute[1],
                    value: startAttribute[3] || startAttribute[4] || startAttribute[5]
                })
                // console.log(match);
            }
            if(startToEnd) { // 这里用while为什么不行?
                advance(startToEnd[0].length) // 删掉标签的 '>'
            }
            return match;
        }

        // console.log(start); 
        // console.log(html); 
        // console.log('-----以上为截取 <div 里面的属性-------'); 

        return false;
    }

    function advance(n) {
        html = html.substring(n);
    }

}

实际效果

test.png