菜鸡手写vue(七)-组件渲染

834 阅读2分钟

说明

vue是怎么渲染一个组件的,是怎么将组件和vue实例渲染区分开来。vue组件实际上是vue的子类,都是通过继承vue而来。通常会使用vue.component()声明一个组件,而这时候声明的组件是一个全局组件,他与vue实例局部组件联系是通过原型链联系起来的,所以在使用组件时会优先使用局部组件。vue.component()的核心是vue.extend(),vue.extend会接受一个对象作为参数,然后根据对象创建一个子类组件构造器函数,并返回组件构造器函数,在实例化这个构造器函数后就会调用init()方法初始化这个组件,也就是创建了组件,调用$mount()后会给组件添加一个watcher并渲染出真实dom元素。

使用方式

Vue.component('my-button', {
    template: `
        <div>
            <button>按钮</button>
        </div>
    `
})
const vm = new Vue({
    el: '#app',
    components: {
        'my-button2': {
            template: `
                <button>内部按钮</button>
            `
        }
    }
})

实现过程

创建全局组件

为了能让子组件始终能访问到Vue,需要将Vue存放到Vue.option._base,Vue.component声明的全局组件都会存放到Vue.options.components上,在合并components属性时会对Vue.options.components进行合并。extend()实际上也是反回了Vue的子类,这个子类是用来创建子组件的。

Vue.options._base = Vue;        // 用来保证子组件可以访问到Vue构造函数
Vue.options.components = {};    // 用来存放组件的定义
Vue.component = function(id, definition){
    definition.name = definition.name || id;
    definition = this.options._base.extend(definition);    // 通过对象产生一个组件构造函数,使用this.options._base调用extend是为了保证始终都是通过调用Vue.extend(),保证extend里面的this指向Vue
    this.options.components[id] = definition;              // 把组件构造函数放在Vue.options里面,实例化Vue的时候会对components属性进行合并
}
let cid = 0;
Vue.extend = function(options){
    const Super = this;         // 永远指向Vue,保证Sub永远都是继承于Vue
    const Sub = function VueComponent(options){
        this._init(options);
    }
    Sub.cid = cid++;            // 给每个组件标号,区分组件
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.component = Super.component;
    // ...

    Sub.options = mergeOptions(Super.options, options);
    return Sub;
}

生成组件的虚拟dom

再根据标签名生成虚拟dom时,标签名可能是我们的自定义组件,因此需要对标签名区分处理,只要不是普通标签那就识别为自定义组件。

export function createElement(vm, tag, data={}, ...children){
    if(isReservedTag(tag)){     // 普通标签
        return vnode(vm, tag, data, children, data.key, null);
    }else{                      // 自定义组件
        const Ctor = vm.$options.components[tag];
        return createComponent(vm, tag, data, children, data.key, Ctor)
    }
}

function makeUp(str){
    const map = {};
    str.split(',').forEach(tag => {
        map[tag.trim()] = true;
    })
    return (tag) => map[tag] || false;
}
export const isReservedTag = makeUp("html,body,base,head,link,meta,style,title," +
"address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section," +
"div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul," +
"a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby," +
"s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video," +
"embed,object,param,source,canvas,script,noscript,del,ins," +
"caption,col,colgroup,table,thead,tbody,td,th,tr," +
"button,datalist,fieldset,form,input,label,legend,meter,optgroup,option," +
"output,progress,select,textarea," +
"details,dialog,menu,menuitem,summary," +
"content,element,shadow,template,blockquote,iframe,tfoot");

Ctor可能是对象也可能是组件构造器函数,如果是来自Vue实例对象内的component属性,则Ctor是对象,因此需要对Ctor转换为组件构造函数;如果是来自Vue.component()声明的全局组件,则Ctor是组件构造器函数。

function createComponent(vm, tag, data, children, key, Ctor){
    // 需要将Ctor转换为组件构造器函数
    if(utils.isObject(Ctor)){
         Ctor = vm.$options._base.extend(Ctor);
         console.log(Ctor.options)
    }
    // 给组件添加生命周期
    data.hook = {
        init(vnode){
            const child = vnode.componentInstance = new vnode.componentOptions.Ctor({});
            child.$mount();
        }    
    }
    // 组件的虚拟节点有hook和当前组件的componentOptions 存放了Ctor组件构造函数
    return vnode(vm, `vue-component-${Ctor.cid}-${tag}`, data, undefined, key, undefined, {Ctor});
}

生成组件的真实dom

虚拟dom转变为真实dom是在patch里面做处理的,因此需要改一下patch的逻辑,针对自定义组件可以直接调用createElm创建元素,不需要替代旧元素。

export function patch(oldVnode, vnode){
    // 没有传oldVnode,说明可能是渲染一个组件,没有指定挂载元素
    if(!oldVnode){
        return createElm(vnode);
    }
}

如果是组件元素,就调用组件的init方法,那么组件就会被实例化,然后调用了实例化对象的mount()方法,组件的真实dom元素就会被渲染在mount()方法,组件的真实dom元素就会被渲染在el上了。

function createElm(vnode){
    let {tag, data, children, text} = vnode;
    if(typeof tag === 'string'){    // 元素,可能是普通元素或者自定义组件
        if(createComponent(vnode)){
            // 返回组件的真实dom元素
            return vnode.componentInstance.$el;
        }

        vnode.el = document.createElement(tag);
        updateProperties(vnode);
        children.forEach(child => {
            vnode.el.appendChild(createElm(child)); // 递归渲染
        })
    }else{                          // 文本
        vnode.el = document.createTextNode(text);
    }
    // 虚拟节点创建真实节点
    return vnode.el;    
}

function createComponent(vnode){
    let i = vnode.data;
    if((i = i.hook) && (i = i.init)){
        i(vnode);   // 调用组件的初始方法,初始后就可以获取到的组件的dom元素vnode.componentInstance.$el
    }
    if(vnode.componentInstance){
        return true;
    }

    return false
}

效果

源代码: image.png 页面渲染: image.png