菜鳥初探Vue源碼(四)-- 組件化

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()的其中一個參數的屬性傳入,等待調用。還調用了下面提到的installComponentHooksapp

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到此結束。接下來進入createComponentui

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.parentoptions.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
    // ...
}
複製代碼

綜上發現,嵌套組件的掛載順序爲子組件先掛載,父組件後掛載。

相關文章
相關標籤/搜索