--本文采自本人公衆號《猴哥別瞎說》vue
MVVM框架中,組件化是一種必要性的存在。node
經過組件化,將頁面作切割,並將其對應的邏輯作必定的抽象,封裝成獨立的模塊。react
那麼Vue中的組件化是怎樣作的呢?咱們分幾個點來具體闡述這個過程:組件聲明過程Vue.component()
的具體實現,組件的建立與掛載過程。bash
Vue.component()
的聲明是一個全局API,咱們在/core/index.js
中查看函數initGlobalAPI()
的代碼:框架
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
export function initGlobalAPI (Vue: GlobalAPI) {
//...省略
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
複製代碼
發現Vue.component()
的聲明沒有明確寫在代碼裏,其聲明的過程是動態的。具體過程在initAssetRegisters()
中:dom
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
// ASSET_TYPES : ['component','directive','filter']
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
//component的特殊處理
if (type === 'component' && isPlainObject(definition)) {
//指定name
definition.name = definition.name || id
//轉換組件配置對象爲構造函數
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
//全局註冊:options['components'][id] = Ctor
//此處註冊以後,就能夠在全局其餘地方使用
this.options[type + 's'][id] = definition
return definition
}
}
})
}
複製代碼
以一個例子來講明代碼的過程吧:async
// 定義一個名爲 button-counter 的新組件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
複製代碼
組件的聲明過程是:先獲取組建的名稱,而後將{data:"...", template: "...}
轉變爲構造函數,最後,將該組件的構造函數註冊到 this.options.components
中。函數
當其餘組件使用到該組件的時候,首先會在this.options.components
中查詢是否存在該組件,由此實現了全局註冊。組件化
##自定義組件的建立與掛載 那麼,咱們想問:這個自定義組件,是在何時建立的?ui
首先建立的是根組件,首次_render()時,會獲得整棵樹的VNode結構,其中必然包括了子組件的建立過程。那麼子組件的建立會是在哪一步呢?讓咱們來回顧根組件的建立過程:
new Vue() => $mount() => vm._render() => _createElement() => createComponent()
複製代碼
在_render()
的過程當中,會觸發_createElement()
。在該函數內部,會對是不是自定義組件進行查詢,若是是自定義組件,那麼會觸發createComponent()
,其過程爲:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
...//省略
// 核心:vnode的生成過程
// 傳入tag多是原生的HTML標籤,也多是用戶自定義標籤
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
// 是原生保留標籤,直接建立VNode
if (config.isReservedTag(tag)) {
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}else if((!data||!data.pre)&&isDef(Ctor=resolveAsset(context.$options, 'components',tag))) {
// 查看this.options.components中是否存在對應的tag的構造函數聲明
// 若存在,須要先建立組件,再建立VNode
vnode = createComponent(Ctor, data, context, children, tag)
} else {
//
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
...//省略
}
複製代碼
那麼咱們來看createComponent()
,它的主要工做是根據構造函數Ctor獲取到VNode:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// 省略...
// 安裝組件的管理鉤子到該節點上
// 這些管理鉤子,會在根組件首次patch的時候調用
installComponentHooks(data)
// 返回VNode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// 省略...
return vnode
}
複製代碼
installComponentHooks()
具體作了什麼事情呢?所謂的組件的管理鉤子,究竟是些啥東西啊?咱們來具體看看:
const componentVNodeHooks = {
// 初始化鉤子:建立組件實例、執行掛載
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
//建立自定義組件VueComponent實例,並和對應VNode相互關聯
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
//建立以後,馬上執行掛載
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
...
},
insert (vnode: MountedComponentVNode) {
...
},
destroy (vnode: MountedComponentVNode) {
...
}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
//將自定義組件相關的hooks都放在生成的這個VNode的data.hook中
//在未來的根組件首次patch過程當中,自定義組件經過這些hooks完成建立,並當即掛載
function installComponentHooks (data: VNodeData) {
const hooks = data.hook || (data.hook = {})
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i]
const existing = hooks[key]
const toMerge = componentVNodeHooks[key]
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
}
}
}
複製代碼
看代碼可知,管理鉤子一共有四個:init()
,prepatch()
,insert()
,destroy()
。看init()
的過程:建立自定義組件VueComponent實例,並和對應VNode相互關聯,而後馬上執行$mount()
方法。
在installComponentHooks()
過程當中,其實只是將這四個管理鉤子放在生成的這個VNode的data.hook
中存放。對應的管理鉤子調用,在首次執行update()時候,執行patch()
的過程裏。詳細的過程在/core/instance/vdom/patch.js
的createElm()
中:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
//省略...
//子組件的生成過程
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
//原生標籤的生成過程
//省略...
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
//獲取data
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
//i就是vnode.data.hook.init()方法,在此處建立自定義組件實例,並完成掛載工做
//詳細見'./patch.js componentVNodeHooks()'
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
//判斷自定義組件是否實例化完成
if (isDef(vnode.componentInstance)) {
//自定義組件實例化後,須要完善屬性、樣式等
initComponent(vnode, insertedVnodeQueue)
//插入到父元素的旁邊
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
複製代碼
能夠看到,自定義組件的建立過程在patch.js
的createComponent()
方法中。經過調用上面提到的vnode.data.hook.init()
方法(和文章的上一段聯繫起來),將自定義組件在此處建立,而且當即調用$mount()
。
這個時候就能夠理解如下結論:
組件建立順序自上而下
組件掛載順序自下而上