详解vue 组件的实现原理
组件机制的设计,可以让开发者把一个复杂的应用分割成一个个功能独立组件,降低开发的难度的同时,也提供了极好的复用性和可维护性。本文我们一起从源码的角度,了解一下组件的底层实现原理。
组件注册时做了什么?
在Vue中使用组件,要做的第一步就是注册。Vue提供了全局注册和局部注册两种方式。
全局注册方式如下:
Vue.component('my-component-name',{/*...*/})
局部注册方式如下:
varComponentA={/*...*/}
newVue({
el:'#app',
components:{
'component-a':ComponentA
}
})
全局注册的组件,会在任何Vue实例中使用。局部注册的组件,只能在该组件的注册地,也就是注册该组件的Vue实例中使用,甚至Vue实例的子组件中也不能使用。
有一定Vue使用经验的小伙伴都了解上面的差异,但是为啥会有这样的差异呢?我们从组件注册的代码实现上进行解释。
//Vue.component的核心代码
//ASSET_TYPES=['component','directive','filter']
ASSET_TYPES.forEach(type=>{
Vue[type]=function(id,definition
){
if(!definition){
returnthis.options[type+'s'][id]
}else{
//组件注册
if(type==='component'&&isPlainObject(definition)){
definition.name=definition.name||id
//如果definition是一个对象,需要调用Vue.extend()转换成函数。Vue.extend会创建一个Vue的子类(组件类),并返回子类的构造函数。
definition=this.options._base.extend(definition)
}
//...省略其他代码
//这里很关键,将组件添加到构造函数的选项对象中Vue.options上。
this.options[type+'s'][id]=definition
returndefinition
}
}
})
//Vue的构造函数
functionVue(options){
if(process.env.NODE_ENV!=='production'&&
!(thisinstanceofVue)
){
warn('Vueisaconstructorandshouldbecalledwiththe`new`keyword')
}
this._init(options)
}
//Vue的初始化中进行选项对象的合并
Vue.prototype._init=function(options){
constvm=this
vm._uid=uid++
vm._isVue=true
//...省略其他代码
if(options&&options._isComponent){
initInternalComponent(vm,options)
}else{
//合并vue选项对象,合并构造函数的选项对象和实例中的选项对象
vm.$options=mergeOptions(
resolveConstructorOptions(vm.constructor),
options||{},
vm
)
}
//...省略其他代码
}
以上摘取了组件注册的主要代码。可以看到Vue实例的选项对象由Vue的构造函数选项对象和Vue实例的选项对象两部分组成。
全局注册的组件,实际上通过Vue.component添加到了Vue构造函数的选项对象Vue.options.components上了。
Vue在实例化时(newVue(options))所指定的选项对象会与构造函数的选项对象合并作为Vue实例最终的选项对象。因此,全局注册的组件在所有的Vue实例中都可以使用,而在Vue实例中局部注册的组件只会影响Vue实例本身。
为啥在HTML模板中可以正常使用组件标签?
我们知道组件可以跟普通的HTML一样在模板中直接使用。例如:
//全局注册一个名为button-counter的组件
Vue.component('button-counter',{
data:function(){
return{
count:0
}
},
template:'Youclickedme{{count}}times.'
})
//创建Vue实例
newVue({
el:'#app'
})
那么,当Vue解析到自定义的组件标签时是如何处理的呢?
Vue对组件标签的解析与普通HTML标签的解析一样,不会因为是非HTML标准的标签而特殊处理。处理过程中第一个不同的地方出现在vnode节点创建时。vue内部通过_createElement函数实现vnode的创建。
exportfunction_createElement( context:Component, tag?:string|Class|Function|Object, data?:VNodeData, children?:any, normalizationType?:number ):VNode|Array { //...省略其他代码 letvnode,ns if(typeoftag==='string'){ letCtor ns=(context.$vnode&&context.$vnode.ns)||config.getTagNamespace(tag) //如果是普通的HTML标签 if(config.isReservedTag(tag)){ vnode=newVNode( config.parsePlatformTagName(tag),data,children, undefined,undefined,context ) }elseif((!data||!data.pre)&&isDef(Ctor=resolveAsset(context.$options,'components',tag))){ //如果是组件标签,e.g.my-custom-tag vnode=createComponent(Ctor,data,context,children,tag) }else{ vnode=newVNode( tag,data,children, undefined,undefined,context ) } }else{ //directcomponentoptions/constructor vnode=createComponent(tag,data,context,children) } if(Array.isArray(vnode)){ returnvnode }elseif(isDef(vnode)){ if(isDef(ns))applyNS(vnode,ns) if(isDef(data))registerDeepBindings(data) returnvnode }else{ returncreateEmptyVNode() } }
以文中的button-counter组件为例,由于button-counter标签不是合法的HTML标签,不能直接newVNode()创建vnode。Vue会通过resolveAsset函数检查该标签是否为自定义组件的标签。
exportfunctionresolveAsset(
options:Object,
type:string,
id:string,
warnMissing?:boolean
):any{
/*istanbulignoreif*/
if(typeofid!=='string'){
return
}
constassets=options[type]
//首先检查vue实例本身有无该组件
if(hasOwn(assets,id))returnassets[id]
constcamelizedId=camelize(id)
if(hasOwn(assets,camelizedId))returnassets[camelizedId]
constPascalCaseId=capitalize(camelizedId)
if(hasOwn(assets,PascalCaseId))returnassets[PascalCaseId]
//如果实例上没有找到,去查找原型链
constres=assets[id]||assets[camelizedId]||assets[PascalCaseId]
if(process.env.NODE_ENV!=='production'&&warnMissing&&!res){
warn(
'Failedtoresolve'+type.slice(0,-1)+':'+id,
options
)
}
returnres
}
button-counter是我们全局注册的组件,显然可以在this.$options.components找到其定义。因此,Vue会执行createComponent函数来生成组件的vnode。
//createComponent exportfunctioncreateComponent( Ctor:Class|Function|Object|void, data:?VNodeData, context:Component, children:?Array , tag?:string ):VNode|Array |void{ if(isUndef(Ctor)){ return } //获取Vue的构造函数 constbaseCtor=context.$options._base //如果Ctor是一个选项对象,需要使用Vue.extend使用选项对象,创建将组件选项对象转换成一个Vue的子类 if(isObject(Ctor)){ Ctor=baseCtor.extend(Ctor) } //如果Ctor还不是一个构造函数或者异步组件工厂函数,不再往下执行。 if(typeofCtor!=='function'){ if(process.env.NODE_ENV!=='production'){ warn(`InvalidComponentdefinition:${String(Ctor)}`,context) } return } //异步组件 letasyncFactory if(isUndef(Ctor.cid)){ asyncFactory=Ctor Ctor=resolveAsyncComponent(asyncFactory,baseCtor) if(Ctor===undefined){ //returnaplaceholdernodeforasynccomponent,whichisrendered //asacommentnodebutpreservesalltherawinformationforthenode. //theinformationwillbeusedforasyncserver-renderingandhydration. returncreateAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } data=data||{} //重新解析构造函数的选项对象,在组件构造函数创建后,Vue可能会使用全局混入造成构造函数选项对象改变。 resolveConstructorOptions(Ctor) //处理组件的v-model if(isDef(data.model)){ transformModel(Ctor.options,data) } //提取props constpropsData=extractPropsFromVNodeData(data,Ctor,tag) //函数式组件 if(isTrue(Ctor.options.functional)){ returncreateFunctionalComponent(Ctor,propsData,data,context,children) } constlisteners=data.on data.on=data.nativeOn if(isTrue(Ctor.options.abstract)){ constslot=data.slot data={} if(slot){ data.slot=slot } } //安装组件hooks installComponentHooks(data) //创建vnode constname=Ctor.options.name||tag constvnode=newVNode( `vue-component-${Ctor.cid}${name?`-${name}`:''}`, data,undefined,undefined,undefined,context, {Ctor,propsData,listeners,tag,children}, asyncFactory ) returnvnode }
由于Vue允许通过一个选项对象定义组件,Vue需要使用Vue.extend将组件的选项对象转换成一个构造函数。
/**
*Vue类继承,以Vue的原型为原型创建Vue组件子类。继承实现方式是采用Object.create(),在内部实现中,加入了缓存的机制,避免重复创建子类。
*/
Vue.extend=function(extendOptions:Object):Function{
//extendOptions是组件的选项对象,与vue所接收的一样
extendOptions=extendOptions||{}
//Super变量保存对父类Vue的引用
constSuper=this
//SuperId保存父类的cid
constSuperId=Super.cid
//缓存构造函数
constcachedCtors=extendOptions._Ctor||(extendOptions._Ctor={})
if(cachedCtors[SuperId]){
returncachedCtors[SuperId]
}
//获取组件的名字
constname=extendOptions.name||Super.options.name
if(process.env.NODE_ENV!=='production'&&name){
validateComponentName(name)
}
//定义组件的构造函数
constSub=functionVueComponent(options){
this._init(options)
}
//组件的原型对象指向Vue的选项对象
Sub.prototype=Object.create(Super.prototype)
Sub.prototype.constructor=Sub
//为组件分配一个cid
Sub.cid=cid++
//将组件的选项对象与Vue的选项合并
Sub.options=mergeOptions(
Super.options,
extendOptions
)
//通过super属性指向父类
Sub['super']=Super
//将组件实例的props和computed属代理到组件原型对象上,避免每个实例创建的时候重复调用Object.defineProperty。
if(Sub.options.props){
initProps(Sub)
}
if(Sub.options.computed){
initComputed(Sub)
}
//复制父类Vue上的extend/mixin/use等全局方法
Sub.extend=Super.extend
Sub.mixin=Super.mixin
Sub.use=Super.use
//复制父类Vue上的component、directive、filter等资源注册方法
ASSET_TYPES.forEach(function(type){
Sub[type]=Super[type]
})
//enablerecursiveself-lookup
if(name){
Sub.options.components[name]=Sub
}
//保存父类Vue的选项对象
Sub.superOptions=Super.options
//保存组件的选项对象
Sub.extendOptions=extendOptions
//保存最终的选项对象
Sub.sealedOptions=extend({},Sub.options)
//缓存组件的构造函数
cachedCtors[SuperId]=Sub
returnSub
}
}
还有一处重要的代码是installComponentHooks(data)。该方法会给组件vnode的data添加组件钩子,这些钩子在组件的不同阶段被调用,例如init钩子在组件patch时会调用。
functioninstallComponentHooks(data:VNodeData){
consthooks=data.hook||(data.hook={})
for(leti=0;i
最后,与普通HTML标签一样,为组件生成vnode节点:
//创建vnode
constvnode=newVNode(
`vue-component-${Ctor.cid}${name?`-${name}`:''}`,
data,undefined,undefined,undefined,context,
{Ctor,propsData,listeners,tag,children},
asyncFactory
)
组件在patch时对vnode的处理与普通标签有所不同。
Vue如果发现正在patch的vnode是组件,那么调用createComponent方法。
functioncreateComponent(vnode,insertedVnodeQueue,parentElm,refElm){
leti=vnode.data
if(isDef(i)){
constisReactivated=isDef(vnode.componentInstance)&&i.keepAlive
//执行组件钩子中的init钩子,创建组件实例
if(isDef(i=i.hook)&&isDef(i=i.init)){
i(vnode,false/*hydrating*/)
}
//init钩子执行后,如果vnode是个子组件,该组件应该创建一个vue子实例,并挂载到DOM元素上。子组件的vnode.elm也设置完成。然后我们只需要返回该DOM元素。
if(isDef(vnode.componentInstance)){
//设置vnode.elm
initComponent(vnode,insertedVnodeQueue)
//将组件的elm插入到父组件的dom节点上
insert(parentElm,vnode.elm,refElm)
if(isTrue(isReactivated)){
reactivateComponent(vnode,insertedVnodeQueue,parentElm,refElm)
}
returntrue
}
}
}
createComponent会调用组件vnode的data对象上定义的init钩子方法,创建组件实例。现在我们回过头来看下init钩子的代码:
//...省略其他代码
init(vnode:VNodeWithData,hydrating:boolean):?boolean{
if(
vnode.componentInstance&&
!vnode.componentInstance._isDestroyed&&
vnode.data.keepAlive
){
//kept-alivecomponents,treatasapatch
constmountedNode:any=vnode//workaroundflow
componentVNodeHooks.prepatch(mountedNode,mountedNode)
}else{
//生成组件实例
constchild=vnode.componentInstance=createComponentInstanceForVnode(
vnode,
activeInstance
)
//挂载组件,与vue的$mount一样
child.$mount(hydrating?vnode.elm:undefined,hydrating)
}
}
//...省略其他代码
由于组件是初次创建,因此init钩子会调用createComponentInstanceForVnode创建一个组件实例,并赋值给vnode.componentInstance。
exportfunctioncreateComponentInstanceForVnode(
vnode:any,
parent:any,
):Component{
//内部组件选项
constoptions:InternalComponentOptions={
//标记是否是组件
_isComponent:true,
//父Vnode
_parentVnode:vnode,
//父Vue实例
parent
}
//checkinline-templaterenderfunctions
constinlineTemplate=vnode.data.inlineTemplate
if(isDef(inlineTemplate)){
options.render=inlineTemplate.render
options.staticRenderFns=inlineTemplate.staticRenderFns
}
//new一个组件实例。组件实例化与newVue()执行的过程相同。
returnnewvnode.componentOptions.Ctor(options)
}
createComponentInstanceForVnode中会执行newvnode.componentOptions.Ctor(options)。由前面我们在创建组件vnode时可知,vnode.componentOptions的值是一个对象:{Ctor,propsData,listeners,tag,children},其中包含了组件的构造函数Ctor。因此newvnode.componentOptions.Ctor(options)等价于newVueComponent(options)。
//生成组件实例
constchild=vnode.componentInstance=createComponentInstanceForVnode(vnode,activeInstance)
//挂载组件,与vue的$mount一样
child.$mount(hydrating?vnode.elm:undefined,hydrating)
等价于:
newVueComponent(options).$mount(hydrating?vnode.elm:undefined,hydrating)
这段代码想必大家都很熟悉了,是组件初始化和挂载的过程。组件的初始化和挂载与在前文中所介绍Vue初始化和挂载过程相同,因此不再展开说明。大致的过程就是创建了一个组件实例并挂载后。使用initComponent将组件实例的$el设置为vnode.elm的值。最后,调用insert将组件实例的DOM根节点插入其父节点。然后就完成了组件的处理。
总结
通过对组件底层实现的分析,我们可以知道,每个组件都是一个VueComponent实例,而VueComponent又是继承自Vue。每个组件实例独立维护自己的状态、模板的解析、DOM的创建和更新。篇幅有限,文中只分析了基本的组件的注册解析过程,未对异步组件、keep-alive等做分析。等后面再慢慢补上。
以上就是详解vue组件的实现原理的详细内容,更多关于vue组件的资料请关注毛票票其它相关文章!