四、vue的模板编译

128 阅读1分钟

前言

vue的模板编译就是

1、将template转化成 ast语法树; 2、优化标记静态节点 (patchFlag,BlockTree) 3、将 ast 转换成 render方法

1、创造一个vue的实例

     <div id="app"><span class="a">name</span>{{name}}</div>
     const vm = new Vue({
            el:'#app',
            data() {
                return {
                    name: 'xiaoming',
                }
            },
        });

2、根据情况确定使用模板

看生命周期描述

QQ截图20220204164530.png

 Vue.prototype.$mount = function (el) {
        const vm = this;
        const options = vm.$options
        el = document.querySelector(el);
        
        if(!options.render){ //看是否有render 属性
            let template = options.template;
            if(!template && el){ // 没有写模板但是写了el ,就去取el作为模板
                template = el.outerHTML; // <div id="app"><span>{{name}}</span>{{age}}</div>
            }
            let render = compileToFunction(template);
            options.render = render;
        }
    }

3、调用 compileToFunction 生成一个render

export function compileToFunction(html) {
    // 编译流程有三个部分  1.把模板变成ast语法树   
    //                   2.优化标记静态节点 (patchFlag,BlockTree) 
    //                   3.把ast变成render函数
    const ast = parserHTML(html);
    
    // 2.优化标记静态节点


    // 3.将ast变成render函数  你要把刚才这棵树 用字符串拼接的方式 变成render函数
    
    const code = generate(ast); // 根据ast生成一个代码字符串
    
    // 通过 new Function + with 的方式,将一个code 字符串 变为可执行的函数
    
    const render = new Function(`with(this){return ${code}}`);
    
    return render;
}

3.1 parserHTML 解析html字符串,将模板变为ast语法树

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


export function parserHTML(html) {
    function advance(len) {
        html = html.substring(len);
    }

    function parseStartTag() {
        const start = html.match(startTagOpen);
        if (start) {
            const match = {
                tagName: start[1],
                attrs: []
            }
            advance(start[0].length);
            let attr;
            let end;
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] });
                advance(attr[0].length);
            }
            advance(end[0].length);
            return match;
        }
        return false;

    }
    let root = null;
    let stack = [];
    let parent = null;
    function createAstElement(tag, attrs) {
        return {
            tag,
            type: 1,
            attrs,
            children: [],
            parent: null
        }
    }
    function start(tagName, attrs) { // 匹配到了开始的标签
        let element = createAstElement(tagName, attrs);
        if (!root) {
            root = element
        }
        let parent = stack[stack.length - 1];
        if (parent) {
            element.parent = parent; // 当放入span的时候 我就知道div是他的父亲
            parent.children.push(element);
        }
        stack.push(element);
    }
    function chars(text) { // 匹配到了开始的标签
        let parent = stack[stack.length - 1];
        text = text.replace(/\s/g,''); // 遇到空格就删除掉
        if(text){
            parent.children.push({
                text,
                type:3
            });
        }
    }
    function end(tagName) {
        stack.pop(); // 每次出去就在栈中删除当前这一项, 这里你可以判断标签是否出错
    }
    while (html) { // html只能由一个根节点
        let textEnd = html.indexOf('<');
        if (textEnd == 0) { // 如果遇到< 说明可能是开始标签或者结束标签
            const startTagMatch = parseStartTag();
            // console.log(startTagMatch)
            if (startTagMatch) { // 匹配到了开始标签
                start(startTagMatch.tagName, startTagMatch.attrs);
                continue
            }
            // 如果代码走到这里了 说明是结束标签
            const endTagMatch = html.match(endTag);
            if (endTagMatch) {
                end(endTagMatch[1]);
                advance(endTagMatch[0].length);
            }
        }
        let text;
        if (textEnd > 0) {
            text = html.substring(0, textEnd)
        }
        if (text) {
            chars(text);
            advance(text.length);
        }
    }
    return root;
}


3.2 调用 generate 函数,将解析完成的html变为函数字符串

遍历 ast 树,拼接成字符串_c("div",{id:"app"},[_c("span",{class:"a"},[_v("name")]),_v(_s(name))])

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // {{  asdasd  }}

function genProps(attrs) {
    let str = '';
    for (let i = 0; i < attrs.length; i++) {
        let attr = attrs[i];
        if (attr.name === 'style') {
            let style = {} // color:red;background:blue
            attr.value.replace(/([^;:]+)\:([^;:]+)/g, function() {
                style[arguments[1]] = arguments[2]
            }); // 如果是sytle 我要将style转换成一个对象
            attr.value = style;
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},`
    }
    return `{${str.slice(0,-1)}}`
}

function gen(el) {
    if (el.type == 1) {
        return generate(el)
    } else {
        let text = el.text
        if (!defaultTagRE.test(text)) {
            return `_v("${text}")`
        } else {  
            let tokens = [];
            let match;
            let lastIndex = defaultTagRE.lastIndex = 0; // 保证每次正则都是从0 开始匹配的
            while (match = defaultTagRE.exec(text)) { // 如果exec + 全局匹配每次执行的时候 都需要还原lastIndex
                let index = match.index; // 匹配到后将前面一段放到tokens中
                if (index > lastIndex) {
                    tokens.push(JSON.stringify(text.slice(lastIndex, index)))
                }
                tokens.push(`_s(${match[1].trim()})`);   // 把当前这一段放到tokens中
                lastIndex = index + match[0].length
            }
            if(lastIndex < text.length){
                tokens.push(JSON.stringify(text.slice(lastIndex)))
            }
            return `_v(${tokens.join('+')})`
        }
    }
}


function genChildren(ast) {
    let children = ast.children; // _c('div',{},'xxx')  _c('div',{},[])
    if (children && children.length > 0) {
        return children.map(child => gen(child)).join(',')
    }
    return false;
}
export function generate(ast) {
    let children = genChildren(ast)
    let code = `_c("${ast.tag}",${
        ast.attrs.length? genProps(ast.attrs) : 'undefined'
    }${
        children? ',['+children+']' : ''
    })`
    return code;
}