Vue源碼探祕(十一)(合併options)

引言

在上一篇文章的結尾,咱們提到在 _init 的最初階段執行的就是 merge options 的邏輯:vue

// src/core/instance/init.js
// 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
  );
}
複製代碼

能夠看到,合併 options 分兩種狀況,它們的區別是什麼呢。node

區別就是在執行用戶編寫的 new Vue(options) 時就會執行 else 邏輯,而執行內部的 new Vue(options)(好比建立子組件實例)時就會走 if 邏輯。ios

這一節咱們就圍繞下面這個例子來研究這兩種狀況下合併 options 分別是怎麼執行的:web

import Vue from "vue";

let childComponent = {
  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",
  renderh => h(childComponent)
});
複製代碼

例子中使用了Vue.mixin函數,是由於mixin自己就是合併 options 的過程,來看 Vue.mixin 的定義:api

// src/core/global-api/mixin.js

import { mergeOptions } from "../util/index";

export function initMixin(Vue: GlobalAPI{
  Vue.mixin = function(mixin: Object{
    this.options = mergeOptions(this.options, mixin);
    return this;
  };
}
複製代碼

能夠看到 Vue.mixin 的內部實現就是調用了 mergeOptions 函數,把 mixin 中的內容合併到 Vue.options 上。數組

外部調用場景

咱們先分析執行外部 new Vue(options) 時的狀況,這時會走 else 邏輯:app

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
);
複製代碼

這裏調用了 resolveConstructorOptions 函數並傳遞了 vm.constructor 做爲參數。resolveConstructorOptions 函數定義在 src/core/instance/init.js 文件中:編輯器

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

這裏的 if 語句經過 Ctor.super 判斷 CtorVue 仍是 Vue 的子類,顯然在咱們的例子中是 Vue ,所以 if 中的邏輯不會執行。因此 resolveConstructorOptions 函數直接返回 Vue.optionside

那這個 Vue.options 又是從哪裏來的呢,實際上它在 initGlobalAPI 函數內被定義:函數

// 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);

  // ...
}
複製代碼

定義 Vue.options 後遍歷 ASSET_TYPES 數組往 Vue.options 添加屬性,ASSET_TYPES 定義以下:

export const ASSET_TYPES = ["component""directive""filter"];
複製代碼

以後又添加了 _base 屬性。此時 Vue.options 大概是這個樣子的:

Vue.options = {
  components: {},
  directives: {},
  filters: {},
  _basefunction Vue(options{}
};
複製代碼

最後經過 extend(Vue.options.components, builtInComponents)把一些內置組件擴展到 Vue.options.components 上,Vue 的內置組件目前有 <keep-alive><transition><transition-group> 組件,這也就是爲何咱們在其它組件中使用 <keep-alive> 組件不須要註冊的緣由,這塊兒後續咱們介紹 <keep-alive> 組件的時候會詳細講。

瞭解完 resolveConstructorOptions 後,咱們分段來分析 mergeOptions 函數:

// src/core/util/options.js

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);

  // ...
}
複製代碼

mergeOptions 函數的 child 參數對應的就是用戶編寫的 options 。這裏首先調用 checkComponents(child) 來檢查 options.components 組件名稱是否合法:

/**
 * Validate component names
 */

function checkComponents(options: Object{
  for (const key in options.components) {
    validateComponentName(key);
  }
}
複製代碼

而後執行一系列 normalize 函數進行規範化操做。這一段代碼不是本節重點,在這裏不會細講。接着看下一段:

export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object 
{
  // ...

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm);
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm);
      }
    }
  }

  // ...
}
複製代碼

最外層的 if 語句代表這是對未合併的 options 的處理,由於註釋提到了只有已合併的 options 纔有 _base 屬性。

if 中的邏輯就是遞歸調用 mergeOptions 函數,將 parent 分別和 child.extendschild.mixins 合併,最後的結果賦給 parent

能夠看到上面這兩段代碼都是在處理 parentchild 參數,mergeOptions 函數核心邏輯是接下來這一段:

export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object 
{
  // ...

  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;
}
複製代碼

這裏遍歷 parent 對象的屬性並調用 mergeField 函數,而後又遍歷了 child 對象的屬性,若是 child 對象的屬性在 parent 中沒有定義,一樣也要調用 mergeField 函數。

mergeField 函數首先定義了 stratstrat 實際上也是個函數,它的取值有兩個來源,咱們先看這個 defaultStrat 的定義:

// src/core/util/options.js

const defaultStrat = function(parentVal: any, childVal: any): any {
  return childVal === undefined ? parentVal : childVal;
};
複製代碼

defaultStrat 的邏輯很簡單,有 childVal 就用 childVal ,沒有就用 parentVal 。咱們再來看 strats 的定義:

/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 */

const strats = config.optionMergeStrategies;
複製代碼

這裏 strats 的值是全局配置對象 configoptionMergeStrategies 屬性,其實就是個空對象。從註釋咱們能夠看出來,strats 就是各類選項合併策略函數的集合,用來合併父 options 和子 options

咱們先來分析一下生命週期函數的合併策略:

// src/shared/constants.js

export const LIFECYCLE_HOOKS = [
  "beforeCreate",
  "created",
  "beforeMount",
  "mounted",
  "beforeUpdate",
  "updated",
  "beforeDestroy",
  "destroyed",
  "activated",
  "deactivated",
  "errorCaptured",
  "serverPrefetch"
];
複製代碼
// src/core/util/options.js

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

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook;
});
複製代碼

能夠看到,LIFECYCLE_HOOKS 定義了全部生命週期函數名,這些都會做爲 strats 的屬性名,全部屬性對應的屬性值都是 mergeHook 這個函數。

mergeHook的最後對res還調用dedupeHooks進行了處理,來看下dedupeHooks函數:

// src/core/util/options.js

function dedupeHooks(hooks{
  const res = [];
  for (let i = 0; i < hooks.length; i++) {
    if (res.indexOf(hooks[i]) === -1) {
      res.push(hooks[i]);
    }
  }
  return res;
}
複製代碼

其實就是數組去重處理,也就是將res中相同的鉤子函數去掉。

到這裏也就印證了咱們上面的猜想:strats 就是各類選項合併策略函數的集合。回到 mergeOptions 函數:

export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object 
{
  // ...

  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;
}
複製代碼

mergeField 函數的下一步操做是將 parentchild 中的 key 合併到 options 中,值是調用對應的合併策略返回的結果。合併完成後 mergeOptions 函數將 options 返回出去。

這樣咱們就把合併 optionselse 邏輯走了一遍。回顧咱們在本節中舉的例子,在通過合併操做後大概是這樣子的:

vm.$options = {
  components: {},
  created: [
    function created({
      console.log("parent created");
    }
  ],
  directives: {},
  filters: {},
  _basefunction Vue(options{
    // ...
  },
  el"#app",
  renderfunction(h{
    //...
  }
};
複製代碼

而咱們例子中的組件 childComponentoptions 合併處理走的是 if 邏輯,接下來咱們就來分析這種狀況。

執行內部組件構造函數

來看 if 邏輯的代碼:

// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
複製代碼

if 邏輯直接調用了 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;
  }
}
複製代碼

函數首先定義了 vm.$options = Object.create(vm.constructor.options) ,這裏的 vm.constructor.options 也就是子構造函數的 options 屬性,它是何時定義的呢。

回顧Vue源碼探祕(九)(createComponent),子構造函數是經過 Vue.extend 建立的:

// src/core/global-api/extend.js

Vue.extend = function(extendOptions: Object): Function {
  // ...

  const Sub = function VueComponent(options{
    this._init(options);
  };
  Sub.options = mergeOptions(Super.options, extendOptions);

  // ...
};
複製代碼

能夠看到,Sub.options 就是由 Vue.options組件 options 經過 mergeOptions 合併的結果。

接着又把實例化子組件傳入的子組件父 VNode 實例 parentVnode、子組件的父 Vue 實例 parent 保存到 vm.$options 中,另外還保留了 parentVnode 配置中的 propsDatalisteners 等屬性。

因此initInternalComponent 函數的邏輯其實很簡單,就是作了一層對象賦值而已。對應咱們的例子,在執行了這個 if 邏輯後大概是這樣子的:

vm.$options = {
  parent: app,
  _parentVnode: VNode,
  propsDataundefined,
  _componentTagundefined,
  _renderChildrenundefined,
  __proto__: {
    components: {},
    directives: {},
    filters: {},
    _basefunction 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>"
  }
};
複製代碼

總結

那麼到這裏,Vue 初始化階段對於 options 的合併過程就介紹完了,options 的合併有兩種方式:

  • 執行外部 new Vue 時,會調用 mergeOptions 函數,並根據不一樣的選項調用不一樣的合併策略函數
  • 子組件實例化時,會調用 initInternalComponent 函數進行合併

下一節咱們一塊兒來看下生命週期部分的源碼實現。

相關文章
相關標籤/搜索