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 函数进行合并

下一节咱们一块儿来看下生命周期部分的源码实现。

相关文章
相关标签/搜索