说明
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方法,那么组件就会被实例化,然后调用了实例化对象的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
}
效果
源代码:
页面渲染: