Vue.js 的另外一個核心思想是組件化。所謂組件化就是將頁面拆分紅多個組件,組件之間資源相互獨立,組件能夠複用,組件之間也能夠嵌套。 接下來以Vue/CLI初始化的代碼爲例,分析一下Vue組件初始化的過程。vue
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App),
}).$mount('#app')
複製代碼
本篇要從_createElement
方法提及(_createElement
-> createElement
-> $createElement
-> render
-> _render
),此處是 render 函數生成 vnode 過程當中差別出現的地方。node
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> {
// 對children作normalization,最終統一形式[vnode, vnode, ...]
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
// tag是字符串
if (typeof tag === 'string') {
// ...
} else {
// tag是組件
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
複製代碼
此處的tag
是一個組件對象,因此會進入else
邏輯,vnode
將由createComponent
方法生成(實際是生成一個佔位 vnode )react
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
// Vue
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// install component management hooks onto the placeholder node
installComponentHooks(data)
// return a placeholder vnode
const name = Ctor.options.name || tag
// args order: tag、data、children、text、elm、context、componentOptions、asyncFactory
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
複製代碼
生成佔位 vnode 過程當中,利用Vue.extend()
將tag
轉化爲子組件的構造器。並做爲new Vnode()
的其中一個參數的屬性傳入,等待調用。還調用了下面提到的installComponentHooks
。app
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const Sub = function VueComponent (options) {
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
// ...此處省略多行代碼
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
複製代碼
以上爲Vue.extend
內部,實現了js的類的繼承,並返回子類構造器。async
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {// ...},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {// ...},
insert (vnode: MountedComponentVNode) {// ...},
destroy (vnode: MountedComponentVNode) {// ...}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
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
}
}
}
複製代碼
以上爲installComponentHooks
內部,給data
上掛了一些鉤子函數,其中就包括隨後要調用的init
。函數
生成佔位 vnode 以後,render 函數執行完畢。進入 _update 函數,一樣會調用 patch ,以後調用到 createElm,在createElm
內部產生了區別。組件化
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) {
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
}
複製代碼
因爲此處的 vnode 爲組件 vnode ,所以在進入 if 判斷的createComponent()
方法內部會返回 true ,createElm
到此結束。接下來進入createComponent
ui
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
if (isDef(i = i.hook) && isDef(i = i.init)) {
//調用init hook
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
}
}
}
複製代碼
在遍歷 data 過程當中發現有 init 鉤子函數,執行 init 。接下來進入init
,接下來的邏輯稍微有些複雜,走的稍微有些遠。咱們先進入 init 一探究竟,等 init 執行結束以後再回到 createComponent 執行組件的插入邏輯。this
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)
// 手動調用$mount方法
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
},
// ...
}
複製代碼
在init
內部,執行createComponentInstanceForVnode(vnode, activeInstance)
,將生成的組件實例賦值給 child 變量,手動調用$mount
方法。spa
其中要說明兩個參數。參數 vnode 爲佔位 vnode ,而 activeInstance 是在 initLifecycle 中定義,_update 中賦值,表示當前激活的組件實例(即當前 vm 實例)。
export function createComponentInstanceForVnode (vnode, parent) {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode, //佔位vnode
parent //當前vm實例
}
// ...
return new vnode.componentOptions.Ctor(options)
}
複製代碼
代碼new vnode.componentOptions.Ctor(options)
其實是調用了真實組件的構造器(上文在生成佔位 vnode 時把子組件的構造器做爲其中一個參數的屬性傳入),今後開始子組件的實例化。
Vue.prototype._init = function (options?: Object) {
if (options && options._isComponent) {
initInternalComponent(vm, options)
}
initLifecycle(vm)
//...
}
複製代碼
此處_isComponent
爲true,進入initInternalComponent
方法。
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
const parentVnode = options._parentVnode //佔位vnode
opts.parent = options.parent //當前vm實例(子組件的父級)
// ...
}
複製代碼
在initInternalComponent
方法中,此時已經進入了子組件的實例化,注意,在函數中定義了子組件實例的 parent 賦值爲父組件實例, parentVnode 賦值爲以前的佔位 vnode 。
export let activeInstance: any = null
export function initLifecycle (vm: Component) {
const options = vm.$options
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
//這裏是子組件初始化,因此vm是子組件實例
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
}
複製代碼
在initLifecycle
中,定義了父子組件關係。vm.$parent = vm.$options.parent
,options.parent.$children.push(vm)
。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevActiveInstance = activeInstance
// activeInstance在此賦值
activeInstance = vm
vm._vnode = vnode // 渲染vnode
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
// ...
}
複製代碼
在_update
中,activeInstance
賦值爲當前子組件的實例。並把以前的activeInstance
賦值給prevActiveInstance
。給vm._vnode
賦值爲子組件的渲染 vnode 。接下來看子組件的 patch 過程。
function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
}
return vnode.elm
}
複製代碼
調用vm.__patch__(vm.$el, vnode, hydrating, false)
此處的 oldVnode 是 undefined,進入 if 判斷。
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) {
if (createComponent(vnode,insertedVnodeQueue,parentElm,refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// 建立DOM
vnode.elm = nodeOps.createElement(tag, vnode)
// 建立children
createChildren(vnode, children, insertedVnodeQueue)
// 插入(parentElm爲空時不作插入操做)
insert(parentElm, vnode.elm, refElm)
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
複製代碼
以上函數中,執行了createChildren
,其實就是若是有子節點,則遞歸地進行以前的流程建立子節點,並插入父節點。等待全部子節點都掛載完畢後,返回到上面的createComponent
,此時componentInstance
爲 true,執行 initComponent
,執行組件的插入。到此爲止,整個組件的初始化結束。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
if (isDef(i = i.hook) && isDef(i = i.init)) {
//調用init hook
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
// 組件的插入在這兒
insert(parentElm, vnode.elm, refElm)
return true
}
}
}
複製代碼
function initComponent (vnode, insertedVnodeQueue) {
// ...
vnode.elm = vnode.componentInstance.$el
// ...
}
複製代碼
綜上發現,嵌套組件的掛載順序爲子組件先掛載,父組件後掛載。