Vue源碼閱讀(七):組件化機制的實現

--本文采自本人公衆號《猴哥別瞎說》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過程

首先建立的是根組件,首次_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()方法。

update過程

installComponentHooks()過程當中,其實只是將這四個管理鉤子放在生成的這個VNode的data.hook中存放。對應的管理鉤子調用,在首次執行update()時候,執行patch()的過程裏。詳細的過程在/core/instance/vdom/patch.jscreateElm()中:

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.jscreateComponent()方法中。經過調用上面提到的vnode.data.hook.init()方法(和文章的上一段聯繫起來),將自定義組件在此處建立,而且當即調用$mount()

這個時候就能夠理解如下結論:

組件建立順序自上而下

組件掛載順序自下而上

相關文章
相關標籤/搜索