Vue2核心原理(简易版)-模版编译

1,010 阅读1分钟

Vue2核心原理(简易版)-模版编译

输入了什么

  • 我们在使用vue.js的时候,查询选项-DOM,可以看到有el,template,render三个选项来进行模版编译,优先级render>template>el,还有一种手动挂载
<div id="app" a=1 style="color:red;background:blue">
    hello {{arr}} world
</div>
let vm = new Vue({
    data() {
        //this = vm
        return {
            arr: { name: 'zf' }
        }
    }
});
vm.$mount('#app')

我们的预期

  • 提取出id="app"这个元素的HTML模板编译成渲染函数

正题,是怎么实现的?

先来看下我画的一张思维导图,大概看一眼

核心策略就是首先用正则对html模版进行词法分析,解析出不同的token,在解析这些token的时候可以得到对应的标签开始、结束以及里面的文本,利用一个栈结构不断的入栈、出栈生成AST树,最后将AST树转化成代码,再构建虚拟dom,进而生成真实dom,patch到页面上去。

  1. 用正则分解出不同的token(tokenizing)。大致有以下的几种token:
    a. 开始标签开始
    b. 标签名
    c. 属性
    d. 开始标签闭合
    e. 文本
    f. 闭合标签
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 标签名 
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //  用来获取的标签名的 match后的索引为1的
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配开始标签的 
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配闭合标签的
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // a=b  a="b"  a='b'
const startTagClose = /^\s*(\/?)>/; //     />   <div/>
  1. 解析后的token用栈结构生成AST树(parse)。根据分解时的不同词法token,我们可以得到对应的如下流程:
    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 end;
                // 如果没有遇到标签结尾就不停的解析
                let 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;
            }
            return false; // 不是开始标签
        }
        while (html) { // 看要解析的内容是否存在,如果存在就不停的解析
            let textEnd = html.indexOf('<'); // 当前解析的开头  
            if (textEnd == 0) {
                const startTagMatch = parseStartTag(html); // 解析开始标签
                if (startTagMatch) {
                    start(startTagMatch.tagName, startTagMatch.attrs)
                    continue;
                }
                const endTagMatch = html.match(endTag);
                if (endTagMatch) {
                    end(endTagMatch[1]);
                    advance(endTagMatch[0].length);
                    continue;
                }
            }
            let text; // //  </div>
            if (textEnd > 0) {
                text = html.substring(0, textEnd)
            }
            if (text) {
                chars(text);
                advance(text.length);
            }
        }
    
        return root;
    }
    
    对应的,我们根据分解时获取到的不同状态,可以得到如下的生成过程:
    let root = null;
    let stack = [];
    function start(tagName, attributes) {
        let parent = stack[stack.length - 1];
        let element = createAstElement(tagName, attributes);
        if (!root) {
            root = element;
        }
        if(parent){
            element.parent = parent;// 当放入栈中时 继续父亲是谁
            parent.children.push(element)
        }
        stack.push(element);
    }
    function end(tagName) {
        let last = stack.pop();
        if (last.tag !== tagName) {
            throw new Error('标签有误');
        }
    }
    function chars(text) {
        text = text.replace(/\s/g, "");
        let parent = stack[stack.length - 1];
        if (text) {
            parent.children.push({
                type: 3,
                text
            })
        }
    }
    
  2. 生成代码(codegen)。我们最后生成的代码是一种给render函数运行的字符串形式_c('div',{id:'app',a:1},_c('span',{},'world'),_v(text))。其中_c方法的三个参数分别代表了标签名、属性键值对的集合、子元素集合。具体过程如下图:
    _c和_v方法就是分别生成元素节点和文本节点vdom的两个方法,我们将这两个方法和我们生成的render函数挂载到Vue原型,这样我们就可以随处使用啦!
    function createElement(vm, tag, data = {}, ...children) {
        return vnode(vm, tag, data, data.key, children, undefined);
    }
    
    function createTextElement(vm, text) {
        return vnode(vm, undefined, undefined, undefined, undefined, text);
    }
    
    function vnode(vm, tag, data, key, children, text) {
        return {
            vm,
            tag,
            data,
            key,
            children,
            text,
            // .....
        }
    }
    
    Vue.prototype._c = function(){ // createElement
        return createElement(this,...arguments)
    }  
    Vue.prototype._v = function (text) { // createTextElement
        return createTextElement(this,text)
    }
    Vue.prototype._s = function(val){ // stringify 
        if(typeof val == 'object') return JSON.stringify(val)
        return val;
    }
    Vue.prototype._render = function(){
       const vm = this;
       let render =  vm.$options.render; // 就是我们解析出来的render方法,同时也有可能是用户写的
       let vnode =  render.call(vm);
       return vnode;
    }
    
  3. 挂载组件($mount)。最后,当组件更新或者是$mount的时候,我们就可以调用vm._render()获取到我们的vdom,再进行diff等操作,生成真实dom渲染到浏览器上了!
    export function mountComponent(vm, el) {
    
        // 更新函数 数据变化后 会再次调用此函数
        let updateComponent = () => {
            // 调用render函数,生成虚拟dom
            vm._update(vm._render()); // 后续更新可以调用updateComponent方法
            // 用虚拟dom 生成真实dom
        }
        updateComponent();
     }
    

完 🎉