分析Vue.componet源码
使用Vue.extend,使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。实际上就是生成一个继承Vue的子类,从而继承Vue的原型方法,在调用原型上面的_init方法,进行子类的初始化,在手动调用$mount方法进行挂载。
而Vue.component内部就是使用Vue.extend方法,我们看一下源码:
export function initGlobalApi(Vue){
Vue.options = {} // Vue.components Vue.directive
// 混合 将mixin上面的合并到Vue上面
Vue.mixin = function(mixin){
// console.log('mixin',this.options)
// 合并对象 先考虑生命周期 不考虑其他的合并 data computed watch
this.options = mergeOptions(this.options,mixin)
// console.log('this.options',this.options)
}
// 用户 new Vue的时候({created(){}}) 也需要合并
Vue.options._base = Vue; // Vue的构造函数 _base 最终的Vue构造函数保留在options对象中
Vue.options.components = {}
initExtend(Vue)
Vue.component = function(id,definition){
// Vue.extend
definition.name = definition.name || id; // 默认会以name属性为准
// 根据当前的组件对象 生成了一个子类的构造函数
// 用的时候的得new definition().$mount()
definition = this.options._base.extend(definition) // 永远是父类
Vue.options.components[id] = definition;
}
}
export function initExtend(Vue){
let cid = 0;
// 核心就是创造子类继承我们的父类
Vue.extend = function(extendOptions){
const Super = this;
const Sub = function VueComponent(options){
this._init(options)
}
Sub.cid = cid++;
// 子类继承父类原型上面的方法
Sub.prototype = Object.create(Super.prototype)
// Sub.prototype.__proto = Super.prototype
Sub.prototype.constructor = Sub;
// 现将全局的Vue父类的options与自身的合并在初始化的时候,将
// Sub.options与传入的options合并
Sub.options = mergeOptions(Super.options,extendOptions)
// console.log('Sub',Sub)
// 处理其他的属性 mixin component
Sub.components = Super.components;
return Sub;
}
}
这段代码是在Vue初始化的时候,会初始化一下全局的api,主要定义一些Vue的静态方法,其中包括混合以及对组件component方法的定义。
1.首先在Vue构造函数上面定义一个options对象对象包括两个属性一个是_base:Vue本身,以及components对象:存放组件对象的。
2.调用initExtend方法,并且传递Vue参数,在initExtend方法中定一个Vue构造函数的静态方法extend这个方法就是Vue.component的核心。在这个方法中:
(1)定义Sub构造函数,并且Sub构造函数继承Super也就是Vue构造函数
Object.create方法就是个生成一个对象,这个对象的__proto__属性指向Object.create内部调用的对象。这里也就是Sub.prototype.proto = Super.prototype; Sub通过原型链继承,但是这样Sub的原型的构造函数就会等于Super,所以我们给Sub重新指定 构造函数等于Sub。即: Sub.prototype.constructor = Sub;
(2)第二部调用mergeOptions方法这个方法是合并属性方法将父类Vue构造函数上面的属性和子类进行合并。
(3)处理其他的属性 比如 components,mixin等等,也就是子类继承父类静态属性方法。
(4)Vue.extend方法中返回这个继承的子类。
3.定义Vue.component静态方法,该方法中有两个参数一个是id:vue组件的id它代表组件的name,第二个参数definition就是参数对象,包括template,data,钩子函数等等。
这个方法中会给definition赋值一个属性name表示组件的name,以及调用g刚才定义的Vue.extend,即生成一个子类重新赋值给definition参数,并将Vue.options.components中增加这个子类。
好Vue.component方法看完了,我们来看到底是怎么将子组件渲染到页面上去的。
子组件是如何显示到页面上面的
我们知道vue页面渲染要经过,生成_render函数以及,调用_render函数,生成虚拟dom,然后根据虚拟dom生成真实的dom挂载到页面上面,那么组件在生成虚拟的dom的时候和普通标签是不一样的,我们看一下源码:`
function createElement(vm,tag,data={},...children){
// 如果是组件 我产生虚拟节点时需要把组件的构造函数传入
// 根据tag 名字 需要判断他是不是个组件
if(isReservedTag(tag)){
// 原生标签
return vnode(tag,data,data.key,children)
}else{
// 合并后的属性
let Ctor = vm.$options.components[tag]; // 获取到这个
// 创建组件的虚拟节点
// children 是组件的插槽
return createComponent(vm,tag,data,data.key,children,Ctor)
}
}
function createComponent(vm,tag,data,key,children,Ctor){
// Ctor有可能是构造函数有可能是个对象
const baseCtor = vm.$options._base; // Vue;
if(typeof Ctor == 'object'){
Ctor = baseCtor.extend(Ctor)
}
// console.log('Ctor',Ctor)
// 给组件增加生命周期
data.hook = {
init(vnode){
let child = vnode.componentIntance = new Ctor({}); // 创建组件的实例
child.$mount() // 组件的挂载逻辑 组建的$mount方法是不传递参数的
} //稍后初始化组件时,会调用此init方法
}
let component = vnode(`vue-component-${Ctor.cid}-${tag}`,data,key,undefined,undefined,{Ctor,children})
// console.log('component',component)
return component
}
在创建虚拟dom的时候,会先判断是否是普通标签如果是普通标签直接走创建虚拟dom的逻辑,如果不是那么就是组件标签,如果是组件标签那么根据tag获取组件的定义,这里要注意有可能是在Vue上面定义的全局组件也有可能是局部组件,所以在vue初始化的时候合并组件对象我们要采用就近原则。
strats.components = function(parentValue,childValue){
const res = Object.create(parentValue);
// res.__proto__ = parentValue 会像原 型链上面去找
if(childValue){
for(let key in childValue){
res[key] = childValue[key]
}
}
return res
}
这块就是合并component对象的我们将父类Vue上面定义的组件作为子类的__proto__上面的对象。
接着:开始创建组件的虚拟dom,调用createComponent方法,如果Ctor是对象的话,那么就调用Vue的extend 将它变成Vue的子类构造函数,然后给组件的属性增加hook属性,这个属性增加hook方法,这个方法是用于初始化Vue子类,并且进行挂载的,接着将生成的组件虚拟dom返回出去。
生成虚拟dom结束以后,该开始生成真实的dom了
生成真实的dom,先生成最外层的dom,然后遍历children,如果tag存在的话是标签节点的话调用creactComponent方法,`
function creactComponent(vnode){
// debugger
// 调用 hook中的init方法
let i = vnode.data
if((i = i.hook) && (i = i.init)){
i(vnode) // 内部会去new这个组件 new晚以后 会将实例挂载在vnode上面
} // i就是init方法
// 是否是虚拟节点
if(vnode.componentIntance){
return true;
}
}
这个方法获取虚拟节点的属性并将属性的hook.init方法取出来并且将虚拟节点传递进去,然后调用,这个方法中首先会生成一个子类的实例,刚才前面说了这个子类默认会调用_init方法,在_init方法中,会进行一系列的初始化操作,但是没有
e
l
属
性
,
然
后
进
行
挂
载
el属性,然后进行挂载
el属性,然后进行挂载mount,。挂摘操作直接将生成的子组件的真实节点返回过来。并且j将创建的真实节点赋值给这个vnode虚拟节点componentInstance.$el。这个真实的节点返回给父节点的子节点中插入到子节点上面。
所以总体经过了父类状态初始化,父类挂载,父类的生成虚拟dom(包含父类儿子组件虚拟dom的生成),父类真实节点的创建,子类子组件真实节点创建(这包括子类的初始化,挂载),最后所有真实节点全部创建完成。
下面代码正确输出是什么
Vue.component('my-button',{
// Vue.extend
data(){
return {
a:111
}
},
name:'aa',
template:'<button>{{a}}</button>',
beforeCreate(){
console.log('child beforeCreate')
},
created(){
// 生命周期 就是回调函数 先订阅号 后序会触发此方法
console.log('child created')
},
beforeMount(){
console.log('child beforeMount')
},
mounted(){
console.log('child mounted')
},
})
// options Api 通过一个选项进行配置
Vue.mixin({
created:function a(){
// 生命周期 就是回调函数 先订阅号 后序会触发此方法
// console.log('created...')
}
})
Vue.mixin({
created:function b(){
// 生命周期 就是回调函数 先订阅号 后序会触发此方法
// console.log('created...')
}
})
// 组件的合并策略,就近原则
let vm = new Vue({
el:'#app',
data(){
return {
firstName:'綦',
lastName:'旭'
}
},
components:{
aa:{
template:`<div>a</div>`
},
bb:{
template:`<div>b</div>`
}
},
beforeCreate(){
console.log('parent beforeCreate')
},
created(){
// 生命周期 就是回调函数 先订阅号 后序会触发此方法
console.log('parent created')
},
beforeMount(){
console.log('parent beforeMount')
},
mounted(){
console.log('parent mounted')
},
updated(){
// console.log('updated')
}
// template:``
})
我们知道了组件是怎么渲染的了,那么我们也理解了声明周期是怎么个先后顺序了。
先执行父类的初始化状态之前,然后初始化之后,然后挂载之前,在挂载完成之前会先进行子类的初始化,挂载,所有的子类挂载完成以后,最后是父类挂载完成。
最后结果截图。