Vue2.0原始碼學習(4) - 合併配置

Inès發表於2022-02-20

合併配置

通過之前的原始碼學習,我們已經瞭解到了new Vue主要有兩種場景,第一種就是在外部主動呼叫new Vue建立一個例項,第二個就是程式碼內部建立子元件的時候自行建立一個new Vue例項。但是無論那種new Vue方式,我們都需要進入了Vue._init,執行mergeOptions函式合併配置。為了更直觀,我們整個demo除錯耍耍。

// src\main.js
let childComp = {
  template:"<div>{{msg}}</div>",
  data(){
    return{
      msg:"childComp"
    }
  },
  created(){
    console.log("childComp created");
  },
  mounted(){
    console.log("childComp mounted");
  }
}

Vue.mixin({
  created(){
    console.log("mixin");
  }
})

let app = new Vue({
  el:"#app",
  render: h => h(childComp)
})

我用的時vue-cli3,這裡有個小細節需要注意一下,vue-cli3開發環境預設使用的是runtime版本(node_modules\vue\dist\vue.runtime.esm.js),這個版本是不支援編譯template的,需要用Compiler版本,這個在vue.config.js中配置一下即可,配置程式碼如下:

module.exports = {
    runtimeCompiler: true
}

準備工作搞好了,那麼我們現在開始進入_init函式,看看合併配置是怎麼一個說法。

// src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
    ...
    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),  //vue.options
        options || {},  //new Vue中的options
        vm
      )
    }
    ...
}

外部呼叫場景

上述程式碼中可明顯看出兩中合併配置的情況,我們一開始進入的肯定時非元件模式,也就是else情況。mergeOptions傳入了3個入參,我們先看第一個入參的resolveConstructorOptions方法做了什麼。

// src\core\instance\init.js
export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

入參Ctor = vm.constructor = Vue,Vue沒有父級,所以不會進入到if邏輯,因此這裡返回的就是Vue.options的配置。Vue.options則在初始化的時候就做了定義和配置。

// src\core\global-api\index.js
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue     //createComponent時用到,之前提及過。
extend(Vue.options.components, builtInComponents)   //擴充套件一些內建元件

這裡ASSET_TYPES在src\shared\constants.js有定義

// src\shared\constants.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

然後我們再返回去_init函式分析一下mergeOptions函式:

// src\core\util\options.js
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  ...  
  const options = {}
  let key
  、、
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    //   key沒在parent定義時
    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
}

簡略了部分程式碼,我們先去關注合併的關鍵程式碼。
這邊其實就是遍歷了parent(Vue.options)和child(new Vue中的options),然後遍歷的過程中呼叫了mergeField方法。而該方法先去拿到一個strat函式,這個函式首先是再strats中去找,沒找到就使用defaultStrat預設函式(defaultStrat可自行查閱原始碼),我們主要看strats:

// src\core\util\options.js
const strats = config.optionMergeStrategies

strats是定義在config中,所以說我們是可以隨意改動strats的。然後在options.js中,strats擴充套件了很多屬性,每個屬性(key)都是一種合併策略,有興趣的可以一個個研究,因為我們例子是生命週期的合併,所以我們先挑生命週期的合併策略來分析,後面遇到其他的再做分析。

// src\core\util\options.js
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

LIFECYCLE_HOOKS定義在src\shared\constants.js

// src\shared\constants.js
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]

遍歷這些值,然後定義它們的合併策略,其實都mergeHook方法,都是一樣的合併策略,下面我們看看mergeHook函式:

// src\core\util\options.js
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

這個多層巢狀的三元表示式看著複雜,其實不難,我們可以分段理解:
①:childVal有值:進入②,
       childVal沒值:賦值parentVal;
②:parentVal有值:parentVal和childVal陣列合並,
       parentVal沒值:進入③;
③:childVal是個陣列:賦值childVal,
       childVal不是陣列:賦值[childVal];
最終我們return了一個陣列到mergeOptions函式。

現在我們回過頭來demo中的Vue.mixin定義,其原始碼其實也呼叫了mergeOptions,我們看看原始碼:

// src\core\global-api\mixin.js
export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

mixin的原始碼很簡單,其實就是呼叫了mergeOptions對Vue.options做了合併。有個小細節需要留意,就是demo中Vue.mixin和new Vue的程式碼順序,必須先對Vue.mixin做出定義,不然在new Vue的時候Vue.options和new Vue的options合併時,是會丟失掉Vue.mixin的,因為那時候Vue.mixin並沒有執行mergeOptions把options合併到Vue.options上。

元件場景

接下來我們看另一種情況,元件合併配置。也就是在_inti方法中執行了initInternalComponent函式,我們來分析一下它做了什麼?

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

子元件的合併就相對簡單很多了,vm.$options去繼承了子元件構造器vm.constructor.options,然後再把一些配置掛載到上面。我們主要看看vm.constructor.options是怎麼來的。

// src\core\global-api\extend.js
Vue.extend = function (extendOptions: Object): Function {
    const Super = this
    ...
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // 構造器指向自己
    Sub.prototype.constructor = Sub
    // 合併配置
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    ...
}

其實Vue.extend的時候對子元件的構造器進行了定義了,還對Vue.options(Super.options)和子元件的options(extendOptions)做了合併。
所以initInternalComponent中的vm.$options其實就是一個已經把Vue.options和子元件的options合併好的配置集合了。

總結

至此Vue的options合併就告一段落了,我們需要知道它有兩個場景,外部呼叫場景和元件場景。
其實一些庫、框架的設計也是類似的,都會有自身的預設配置,同時又允許在初始化的時候讓開發者自定義配置,之後再合併兩個配置來達到應付各種場景需求,這種設計思想也是我們寫元件或做架構的時候必不可少的思維模式。

相關文章