Vue 源碼深入解析之 組件化、createComponent、patch 和 合併配置

一、組件化

  1. Vue.js 另一個核心思想是組件化。所謂組件化,就是把頁面拆分成多個組件 (component),每個組件依賴的 CSS、JavaScript、模板、圖片等資源放在一起開發和維護。組件是資源獨立的,組件在系統內部可複用,組件和組件之間可以嵌套。

  2. 我們在用 Vue.js 開發實際項目的時候,就是像搭積木一樣,編寫一堆組件拼裝生成頁面。在 Vue.js的官網中,也是花了大篇幅來介紹什麼是組件,如何編寫組件以及組件擁有的屬性和特性。我們將從源碼的角度來分析 Vue 的組件內部是如何工作的,只有瞭解了內部的工作原理,才能讓我們使用它的時候更加得心應手。

  3. 接下來我們會用 Vue-cli 初始化的代碼爲例,來分析一下 Vue 組件初始化的一個過程,如下所示:

import Vue from 'vue'
import App from './App.vue'

var app = new Vue({
  el: '#app',
  // 這裏的 h 是 createElement 方法
  render: h => h(App)
})

和之前相同的點也是通過 render 函數去渲染的,不同的這次通過 createElement 傳的參數是一個組件而不是一個原生的標籤,那麼接下來我們就開始分析這一過程。

二、createComponent 的理解

  1. 我們之前在分析 createElement 的實現的時候,它最終會調用 _createElement 方法,其中有一段邏輯是對參數 tag 的判斷,如果是一個普通的 html 標籤,是一個普通的 div,則會實例化一個普通 VNode 節點,否則通過 createComponent 方法創建一個組件 VNode,如下所示:
if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // platform built-in elements
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // component
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    // unknown or unlisted namespaced elements
    // check at runtime because it may get assigned a namespace when its
    // parent normalizes children
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}
  1. 在這裏傳入的是一個 App 對象,它本質上是一個 Component 類型,那麼它會走到上述代碼的 else 邏輯,直接通過 createComponent 方法來創建 vnode。所以接下來我們來看一下 createComponent 方法的實現,它定義在 src/core/vdom/create-component.js 文件中,如下所示:
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder 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
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}
  1. 從上面可以看到,createComponent 的邏輯也會有一些複雜,但是分析源碼比較推薦的是隻分析核心流程,分支流程可以之後針對性的看,所以這裏針對組件渲染這個 case 主要就三個關鍵步驟:
    構造子類構造函數,安裝組件鉤子函數和實例化 vnode

  2. 構造子類構造函數,如下所示:

const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}

我們在編寫一個組件的時候,通常都是創建一個普通對象,還是以我們的 App.vue 爲例,代碼如下:

import HelloWorld from './components/HelloWorld'

export default {
  name: 'app',
  components: {
    HelloWorld
  }
}
  1. 這裏 export 的是一個對象,所以 createComponent 裏的代碼邏輯會執行到 baseCtor.extend(Ctor),在這裏 baseCtor 實際上就是 Vue,這個的定義是在最開始初始化 Vue 的階段,在 src/core/global-api/index.js 中的 initGlobalAPI 函數有這麼一段邏輯:
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
  1. 這裏定義的是 Vue.options,而我們的 createComponent 取的是 context.$options,實際上在 src/core/instance/init.jsVue 原型上的 _init 函數中有這麼一段邏輯:
vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
  1. 這樣就把 Vue 上的一些 option 擴展到了 vm.$options 上,所以我們也就能通過 vm.$options._base 拿到 Vue 這個構造函數了。mergeOptions 的實現我們會在後面具體分析,現在只需要理解它的功能是把 Vue 構造函數的 options 和用戶傳入的 options 做一層合併,到 vm.$options 上。

  2. 在瞭解了 baseCtor 指向了 Vue 之後,我們來看一下 Vue.extend 函數的定義,在 src/core/global-api/extend.js 中,如下所示:

/**
 * Class inheritance
 */
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 name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
    validateComponentName(name)
  }

  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
  )
  Sub['super'] = Super

  // For props and computed properties, we define the proxy getters on
  // the Vue instances at extension time, on the extended prototype. This
  // avoids Object.defineProperty calls for each instance created.
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // allow further extension/mixin/plugin usage
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // create asset registers, so extended classes
  // can have their private assets too.
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  // enable recursive self-lookup
  if (name) {
    Sub.options.components[name] = Sub
  }

  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // cache constructor
  cachedCtors[SuperId] = Sub
  return Sub
}
  1. Vue.extend 的作用就是構造一個 Vue 的子類,它使用一種非常經典的原型繼承的方式把一個純對象轉換一個繼承於 Vue 的構造器 Sub 並返回,然後對 Sub 這個對象本身擴展了一些屬性,如擴展 options、添加全局 API 等;並且對配置中的 propscomputed 做了初始化工作;最後對於這個 Sub 構造函數做了緩存,避免多次執行 Vue.extend 的時候對同一個子組件重複構造。

  2. 這樣當我們去實例化 Sub 的時候,就會執行 this._init 邏輯再次走到了 Vue 實例的初始化邏輯,如下所示:

const Sub = function VueComponent (options) {
  this._init(options)
}
  1. 安裝組件鉤子函數,如下所示:
// install component management hooks onto the placeholder node
installComponentHooks(data)
  1. 我們之前提到 Vue.js 使用的 Virtual DOM 參考的是開源庫 snabbdom,它的一個特點是在 VNodepatch 流程中對外暴露了各種時機的鉤子函數,方便我們做一些額外的事情,Vue.js 也是充分利用這一點,在初始化一個 Component 類型的 VNode 的過程中實現了幾個鉤子函數:
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

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
    }
  }
}

function mergeHook (f1: any, f2: any): Function {
  const merged = (a, b) => {
    // flow complains about extra args which is why we use any
    f1(a, b)
    f2(a, b)
  }
  merged._merged = true
  return merged
}
  1. 整個 installComponentHooks 的過程就是把 componentVNodeHooks 的鉤子函數合併到 data.hook 中,在 VNode 執行 patch 的過程中執行相關的鉤子函數。這裏要注意的是合併策略,在合併過程中,如果某個時機的鉤子已經存在 data.hook 中,那麼通過執行 mergeHook 函數做合併,這個邏輯很簡單,就是在最終執行的時候,依次執行這兩個鉤子函數即可。

  2. 實例化 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

最後一步非常簡單,通過 new VNode 實例化一個 vnode 並返回。需要注意的是和普通元素節點的 vnode 不同,組件的 vnode 是沒有 children 的,這點很關鍵,在之後的 patch 過程中我們會再提。

  1. 總結:這裏我們分析了 createComponent 的實現,瞭解到它在渲染一個組件的時候的三個關鍵邏輯:構造子類構造函數,安裝組件鉤子函數和實例化 vnodecreateComponent 後返回的是組件 vnode,它也一樣走到 vm._update 方法,進而執行了 patch 函數。

三、patch 的理解

  1. 當我們通過 createComponent 創建了組件 VNode,接下來會走到 vm._update,執行 vm.__patch__ 去把 VNode 轉換成真正的 DOM 節點。針對一個普通的 VNode 節點,會有哪些不一樣的地方。

  2. patch 的過程會調用 createElm 創建元素節點,回顧一下 createElm 的實現,它的定義在 src/core/vdom/patch.js 中:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  // ...
}
  1. createComponent,我們刪掉多餘的代碼,只保留關鍵的邏輯,這裏會判斷 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值,如果爲 true 則直接結束,那麼接下來看一下 createComponent 方法的實現,如下所示:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  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, 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
    }
  }
}

createComponent 函數中,首先對 vnode.data 做了一些判斷:

let i = vnode.data
if (isDef(i)) {
  // ...
  if (isDef(i = i.hook) && isDef(i = i.init)) {
    i(vnode, false /* hydrating */)
    // ...
  }
  // ..
}

如果 vnode 是一個組件 VNode,那麼條件會滿足,並且得到 i 就是 init 鉤子函數,回顧上節我們在創建組件 VNode 的時候合併鉤子函數中就包含 init 鉤子函數,定義在 src/core/vdom/create-component.js 中:

init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    // kept-alive components, treat as a patch
    const mountedNode: any = vnode // work around flow
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
},

init 鉤子函數執行也很簡單,我們先不考慮 keepAlive 的情況,它是通過 createComponentInstanceForVnode 創建一個 Vue 的實例,然後調用 $mount 方法掛載子組件,
先來看一下 createComponentInstanceForVnode 的實現:

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}
  1. createComponentInstanceForVnode 函數構造的一個內部組件的參數,然後執行 new vnode.componentOptions.Ctor(options)。這裏的 vnode.componentOptions.Ctor 對應的就是子組件的構造函數,它實際上是繼承於 Vue 的一個構造器 Sub,相當於 new Sub(options) 這裏有幾個關鍵參數要注意幾個點,_isComponenttrue 表示它是一個組件,parent 表示當前激活的組件實例。所以子組件的實例化實際上就是在這個時機執行的,並且它會執行實例的 _init 方法,這個過程有一些和之前不同的地方需要挑出來說,代碼在 src/core/instance/init.js 中:
Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  } 
}

這裏首先是合併 options 的過程有變化,_isComponent 爲 true,所以走到了 initInternalComponent 過程,這個函數的實現也簡單看一下:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}
  1. 這個過程我們重點記住以下幾個點即可:opts.parent = options.parentopts._parentVnode = parentVnode,它們是把之前我們通過 createComponentInstanceForVnode 函數傳入的幾個參數合併到內部的選項 $options 裏了。再來看一下 _init 函數最後執行的代碼:
if (vm.$options.el) {
   vm.$mount(vm.$options.el)
}
  1. 由於組件初始化的時候是不傳 el 的,因此組件是自己接管了 $mount 的過程,回到組件 init 的過程,componentVNodeHooksinit 鉤子函數,在完成實例化的 _init 後,接着會執行 child.$mount(hydrating ? vnode.elm : undefined, hydrating) 。這裏 hydratingtrue 一般是服務端渲染的情況,我們只考慮客戶端渲染,所以這裏 $mount 相當於執行 child.$mount(undefined, false),它最終會調用 mountComponent 方法,進而執行 vm._render() 方法:
Vue.prototype._render = function (): VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  
  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  // render self
  let vnode
  try {
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    // ...
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}
  1. 我們只保留關鍵部分的代碼,這裏的 _parentVnode 就是當前組件的父 VNode,而 render 函數生成的 vnode 當前組件的渲染 vnodevnodeparent 指向了 _parentVnode,也就是 vm.$vnode,它們是一種父子的關係。

  2. 我們知道在執行完 vm._render 生成 VNode 後,接下來就要執行 vm._update 去渲染 VNode 了。來看一下組件渲染的過程中有哪些需要注意的,vm._update 的定義在 src/core/instance/lifecycle.js 中:

export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}
  1. _update 過程中有幾個關鍵的代碼,首先 vm._vnode = vnode 的邏輯,這個 vnode 是通過 vm._render() 返回的組件渲染 VNodevm._vnodevm.$vnode 的關係就是一種父子關係,用代碼表達就是 vm._vnode.parent === vm.$vnode,還有一段比較有意思的代碼:
export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    // ...
    const prevActiveInstance = activeInstance
    activeInstance = vm
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // ...
}

  1. 這個 activeInstance 作用就是保持當前上下文的 Vue 實例,它是在 lifecycle 模塊的全局變量,定義是 export let activeInstance: any = null,並且在之前我們調用 createComponentInstanceForVnode 方法的時候從 lifecycle 模塊獲取,並且作爲參數傳入的。因爲實際上 JavaScript 是一個單線程,Vue 整個初始化是一個深度遍歷的過程,在實例化子組件的過程中,它需要知道當前上下文的 Vue 實例是什麼,並把它作爲子組件的父 Vue 實例。子組件的實例化過程先會調用 initInternalComponent(vm, options) 合併 options,把 parent 存儲在 vm.$options 中,在 $mount 之前會調用 initLifecycle(vm) 方法,如下所示:
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  // ...
}

可以看到 vm.$parent 就是用來保留當前 vm 的父實例,並且通過 parent.$children.push(vm) 來把當前的 vm 存儲到父實例的 $children 中。

  1. vm._update 的過程中,把當前的 vm 賦值給 activeInstance,同時通過 const prevActiveInstance = activeInstanceprevActiveInstance 保留上一次的 activeInstance。實際上,prevActiveInstance 和當前的 vm 是一個父子關係,當一個 vm 實例完成它的所有子樹的 patch 或者 update 過程後,activeInstance 會回到它的父實例,這樣就完美地保證了 createComponentInstanceForVnode 整個深度遍歷過程中,我們在實例化子組件的時候能傳入當前子組件的父 Vue 實例,並在 _init 的過程中,通過 vm.$parent 把這個父子關係保留。
    那麼回到 _update,最後就是調用 __patch__ 渲染 VNode 了,如下所示:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
 
function patch (oldVnode, vnode, hydrating, removeOnly) {
  // ...
  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    // ...
  }
  // ...
}

  1. 這裏又回到了開始的過程,之前分析過負責渲染成 DOM 的函數是 createElm,注意這裏我們只傳了 兩個參數,所以對應的 parentElmundefined,我們再來看看它的定義,如下所示:
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)) {
    // ...

    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      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)
  }
}
  1. 注意,這裏我們傳入的 vnode 是組件渲染的 vnode,也就是我們之前說的 vm._vnode,如果組件的根節點是個普通元素,那麼 vm._vnode 也是普通的 vnode,這裏 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值是 false。接下來的過程就和之前一樣了,先創建一個父節點佔位符,然後再遍歷所有子 VNode 遞歸調用 createElm,在遍歷的過程中,如果遇到子 VNode 是一個組件的 VNode,則重複本節開始的過程,這樣通過一個遞歸的方式就可以完整地構建了整個組件樹。

  2. 由於我們這個時候傳入的 parentElm 是空,所以對組件的插入,在 createComponent 有這麼一段邏輯,如下所示:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // ....
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // ...
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

在完成組件的整個 patch 過程後,最後執行 insert(parentElm, vnode.elm, refElm) 完成組件的 DOM 插入,如果組件 patch 過程中又創建了子組件,那麼DOM 的插入順序是先子後父。

  1. 總結:一個組件的 VNode 是如何創建、初始化、渲染的過程也就介紹完畢了。在對組件化的實現有一個大概瞭解後,接下來我們來介紹一下這其中的一些細節。我們知道編寫一個組件實際上是編寫一個 JavaScript 對象,對象的描述就是各種配置,之前我們提到在 _init 的最初階段執行的就是 merge options 的邏輯,那麼接下來我們從源碼角度來分析合併配置的過程。

四、合併配置

  1. new Vue 的過程通常有兩種場景,一種是外部我們的代碼主動調用 new Vue(options) 的方式實例化一個 Vue 對象;另一種是我們上一節分析的組件過程中內部通過 new Vue(options) 實例化子組件。

  2. 無論哪種場景,都會執行實例的 _init(options) 方法,它首先會執行一個 merge options 的邏輯,相關的代碼在 src/core/instance/init.js 中,如下所示:

Vue.prototype._init = function (options?: Object) {
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
}
  1. 可以看到不同場景對於 options 的合併邏輯是不一樣的,並且傳入的 options 值也有非常大的不同,接下來會從兩種場景的 options 合併過程。爲了更直觀,我們可以舉個簡單的示例,如下所示:
import Vue from 'vue'

let childComp = {
  template: '<div>{{msg}}</div>',
  created() {
    console.log('child created')
  },
  mounted() {
    console.log('child mounted')
  },
  data() {
    return {
      msg: 'Hello Vue'
    }
  }
}

Vue.mixin({
  created() {
    console.log('parent created')
  }
})

let app = new Vue({
  el: '#app',
  render: h => h(childComp)
})
  1. 外部調用場景,當執行 new Vue 的時候,在執行 this._init(options) 的時候,就會執行如下邏輯去合併 options
vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
  1. 這裏通過調用 mergeOptions 方法來合併,它實際上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合併,resolveConstructorOptions 的實現先不考慮,在我們這個場景下,它還是簡單返回 vm.constructor.options,相當於 Vue.options,那麼這個值又是什麼呢,其實在 initGlobalAPI(Vue) 的時候定義了這個值,代碼在 src/core/global-api/index.js 中:
export function initGlobalAPI (Vue: GlobalAPI) {
  // ...
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)
  // ...
}
  1. 首先通過 Vue.options = Object.create(null) 創建一個空對象,然後遍歷 ASSET_TYPESASSET_TYPES 的定義在 src/shared/constants.js 中:
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

所以上面遍歷 ASSET_TYPES 後的代碼相當於:

Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}
  1. 接着執行了 Vue.options._base = Vue,它的作用在我們上節實例化子組件的時候介紹了。

  2. 最後通過 extend(Vue.options.components, builtInComponents) 把一些內置組件擴展到 Vue.options.components 上,Vue 的內置組件目前有 <keep-alive><transition><transition-group> 組件,這也就是爲什麼我們在其它組件中使用 <keep-alive> 組件不需要註冊的原因。

  3. 那麼回到 mergeOptions 這個函數,它的定義在 src/core/util/options.js 中:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
  1. mergeOptions 主要功能就是把 parentchild 這兩個對象根據一些合併策略,合併成一個新對象並返回。比較核心的幾步,先遞歸把 extendsmixins 合併到 parent 上,然後遍歷 parent,調用 mergeField,然後再遍歷 child,如果 key 不在 parent 的自身屬性上,則調用 mergeField

  2. 這裏有意思的是 mergeField 函數,它對不同的 key 有着不同的合併策略。舉例來說,對於生命週期函數,它的合併策略是這樣的,如下所示:

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

這其中的 LIFECYCLE_HOOKS 的定義在 src/shared/constants.js 中:

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]
  1. 這裏定義了 Vue.js 所有的鉤子函數名稱,所以對於鉤子函數,他們的合併策略都是 mergeHook 函數。這個函數的實現也非常有意思,用了一個多層三元運算符,邏輯就是如果不存在 childVal ,就返回 parentVal;否則再判斷是否存在 parentVal,如果存在就把 childVal 添加到 parentVal 後返回新數組;否則返回 childVal 的數組。所以回到 mergeOptions 函數,一旦 parentchild 都定義了相同的鉤子函數,那麼它們會把兩個鉤子函數合併成一個數組。

  2. 通過執行 mergeField 函數,把合併後的結果保存到 options 對象中,最終返回它。因此,在我們當前這個 case 下,執行完如下合併後:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

vm.$options 的值差不多是如下這樣:

vm.$options = {
  components: { },
  created: [
    function created() {
      console.log('parent created')
    }
  ],
  directives: { },
  filters: { },
  _base: function Vue(options) {
    // ...
  },
  el: "#app",
  render: function (h) {
    //...
  }
}
  1. 組件場景,由於組件的構造函數是通過 Vue.extend 繼承自 Vue 的,先回顧一下這個過程,代碼定義在 src/core/global-api/extend.js 中:
/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
  // ...
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )

  // ...
  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // ...
  return Sub
}

我們只保留關鍵邏輯,這裏的 extendOptions 對應的就是前面定義的組件對象,它會和 Vue.options 合併到 Sub.opitons 中。

  1. 接下來子組件的初始化過程,代碼定義在 src/core/vdom/create-component.js 中:
export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // ...
  return new vnode.componentOptions.Ctor(options)
}
  1. 這裏的 vnode.componentOptions.Ctor 就是指向 Vue.extend 的返回值 Sub, 所以 執行 new vnode.componentOptions.Ctor(options) 接着執行 this._init(options),因爲 options._isComponent 爲 true,那麼合併 options 的過程走到了 initInternalComponent(vm, options) 邏輯。先來看一下它的代碼實現,在 src/core/instance/init.js 中:
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}
  1. initInternalComponent 方法首先執行 const opts = vm.$options = Object.create(vm.constructor.options),這裏的 vm.constructor 就是子組件的構造函數 Sub,相當於 vm.$options = Object.create(Sub.options)

  2. 接着又把實例化子組件傳入的子組件父 VNode 實例 parentVnode、子組件的父 Vue 實例 parent 保存到 vm.$options 中,另外還保留了 parentVnode 配置中的如 propsData 等其它的屬性。這麼看來,initInternalComponent 只是做了簡單一層對象賦值,並不涉及到遞歸、合併策略等複雜邏輯。因此,在我們當前這個 case 下,執行完如下合併後:

initInternalComponent(vm, options)

vm.$options 的值差不多是如下這樣:

vm.$options = {
  parent: Vue /*父Vue實例*/,
  propsData: undefined,
  _componentTag: undefined,
  _parentVnode: VNode /*父VNode實例*/,
  _renderChildren:undefined,
  __proto__: {
    components: { },
    directives: { },
    filters: { },
    _base: function Vue(options) {
        //...
    },
    _Ctor: {},
    created: [
      function created() {
        console.log('parent created')
      }, function created() {
        console.log('child created')
      }
    ],
    mounted: [
      function mounted() {
        console.log('child mounted')
      }
    ],
    data() {
       return {
         msg: 'Hello Vue'
       }
    },
    template: '<div>{{msg}}</div>'
  }
}
  1. 總結:Vue 初始化階段對於 options 的合併過程就介紹完了,我們需要知道對於 options 的合併有兩種方式,子組件初始化過程通過 initInternalComponent 方式要比外部初始化 Vue 通過 mergeOptions 的過程要快,合併完的結果保留在 vm.$options 中。縱觀一些庫、框架的設計幾乎都是類似的,自身定義了一些默認配置,同時又可以在初始化階段傳入一些定義配置,然後去 merge 默認配置,來達到定製化不同需求的目的。只不過在 Vue 的場景下,會對 merge 的過程做一些精細化控制,雖然我們在開發自己的 JSSDK 的時候並沒有 Vue 這麼複雜,但這個設計思想是值得我們借鑑的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章