菜鸡手写vue(三)-模版编译

110 阅读3分钟

模版编译

挂载模版

将vue实例挂载在html模版上,通常有三种方式:1、render()函数;2、template模版;3、根据el元素路径进行挂载。优先级如上顺序。最终都会转变成使用render函数编译模版。

Vue.prototype.$mount = function(el){
    const opt = this.$options;
    el = document.querySelector(el);

    // 如果不存在render属性
    if(!opt.render){
        let template = opt.template;
        // 如果不存在tempalte,但是有el;
        if(!template && el){
            template = el.outerHTML;
        }

        // 最终还是需要将template模版转化成render函数
        if(template){
            console.log(template);
            let render = complileToFunction(template);
            opt.render = render;          
        }
    }
}

先对html模版解析生成ast语法树,其实就是使用正则表达式对html字符串进行匹配,匹配的过程中生成一个树状结构的对象,使用js对象来表示一个html结构,那么每个js对象自然也包含tagName、属性、父元素、子元素等。然后再利用ast树生成代码。

生成ast树

借用栈结构来构建父子关系,前一个元素是后一个元素的父节点。

<div>
   <ul>
       <li> </li>
   </ul>
   <p></p>
</div>
/**
     * 原理:假设一棵结构如上。
     *      当遇到开始标签时,将标签压入栈中,一直到遇到结束标签之前[div, ul, li],此时前者都是后者的父节点,在下一次操作
     *      遇到结束</li>就弹出元素,此时栈中[div, ul],再遇到结束标签</ul>继续弹出元素,此时栈中[div],再遇到开始标签<p>,
     *      压入栈中[div, p], 最后遇到结束标签</p></div>,依次弹出元素,最后栈为空了[]。
     */
// 以下为源码的正则
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊标签 形如 abc:234 前面的abc:可有可无
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕获里面的标签名
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束  >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕获里面的标签名
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性  形如 id=true id='app' id="app"

// 将html模版解析为ast语法树
function parseHtml(html){
    const stack= [];
    let root = null;    // 根节点
    function start(tagName, attrs){
        const element = createASTElement(tagName, attrs);
        if(!root){
            // 根节点为空,说明当前是根节点
            root = element;
        }else{
            const parent = stack[stack.length - 1]
            element.parent = parent;
            parent.children.push(element);
        }
        stack.push(element);
    }
    function end(tagName){
        let endTag = stack.pop();
        if(endTag.tag != tagName){
            console.log('标签出错')
        }
    }
    function text(chars){
        chars = chars.replace(/\s/g, '');
        const parent = stack[stack.length - 1];
        if(chars){
            parent.children.push({
                text: chars,
                type: 2,
                parent,
            });
        }
    }
    function createASTElement(tag, attrs, type = 1, parent = null){
        return {
            tag,
            type,
            children: [],
            parent,
            attrs,
        }
    }
    // 前进截取
    function advance(len){
        html = html.substring(len);
    }
    // 解析开始标签
    function parseStartTag(){
        const start = html.match(startTagOpen);
        // 没有匹配到开始标签
        if(!start) return false;
        const match = {
            tagName: start[1],
            attrs: [],
        }
        // 减掉前面已匹配过的字符串,截取剩下的字符串
        advance(start[0].length);

        let end, attr;
        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);
        }
        if(end){
            advance(end[0].length);
        }

        return match;
    }

    // 不停的截取模版,直到模版为空
    while(html){
        let index = html.indexOf('<');
        if(index == 0){
            // 解析开始标签和属性
            const startTagMatch = parseStartTag();
            if(startTagMatch){                        // 开始标签
                start(startTagMatch.tagName, startTagMatch.attrs);
                continue
            }
            let endTagMatch;
            if(endTagMatch = html.match(endTag)){     // 结束标签
                end(endTagMatch[1]);
                advance(endTagMatch[0].length);
                continue
            }
            break;
        }
                                                      // 文本
        if(index > 0){
            let chars = html.substring(0, index);
            text(chars);
            advance(chars.length)
        }
    }
    return root;
}

export default parseHtml;

利用ast树生成代码

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号 {{  }} 捕获花括号里面的内容

function genProps(attrs){
    // 返回形式 ‘{key: value, key: value}’

    let str = "";
    attrs.forEach(obj => {
        if(obj.name === 'style'){
            // 改造style的值为一个对象
            const style = {};
            obj.value.replace(/([^;:]+):([^;:]+)/g, function(){
                style[arguments[1]] = arguments[2];
            })
            obj.value = style;
        }
        str += `${obj.name}:${JSON.stringify(obj.value)},`;
    });

    return `{${str.slice(0, -1)}}`;
}

function genChildren(el){
    let children = el.children;
    if(children){
        return `${children.map(item => gen(item)).join(',')}`;
    }
    return false;
}

function gen(el){
    if(el.type == 1){
        // 如果是元素就递归生成
        return generate(el);
    }else{
        let text = el.text;
        if(!defaultTagRE.test(text)) return `_v('${text}')`;    // 说明只是普通文本
        // 否者就是有表达式

        let tokens = [];
        let lastIndex = (defaultTagRE.lastIndex = 0);   // lastIndex 记录的是上一次匹配到内容的下一个坐标,所以需要重置一下
        let match = null;
        while(match = defaultTagRE.exec(text)){
            let index = match.index;
            if(index > lastIndex){
                // 匹配到 {{ ,保存前面的普通文本
                tokens.push(JSON.stringify(text.slice(lastIndex, index)));
            }
            // 匹配到表达式,保存表达式
            tokens.push(`_s(${match[1].trim()})`);
            lastIndex = index + match[0].length;
        }
        if(lastIndex<text.length){
            // 说明后面还有普通文本
            tokens.push(JSON.stringify(text.slice(lastIndex)));
        }
        return `_v(${tokens.join('+')})`;
    }
}

/**
 * 
 * @param {*} ast 
 * @returns code
 * 将ast树生成代码
 */
export function generate(ast){
    let code = `_c('${ast.tag}', ${
        ast.attrs.length ? genProps(ast.attrs) : 'undefined'
    }${
        ast.children.length ? `,${genChildren(ast)}` : ''
    })`
    return code;
}