【Vue源码】10.组件化-合并配置

649 阅读2分钟

组件化-合并配置

在之前的文章中我们讲过,初始化 Vue 实例有两种方法,一种是手动调用 new Vue(options) 的方法,二是上节讲到的调用 new vnode.componentOptions.Ctor(options) 的方式初始化。 无论哪种方式都会执行到 _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
    );
  }
  // ...
};

下面我们通过一个 🌰 来看一下两种场景的不同合并过程:

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");
  },
});

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

外部调用场景

一: 首先执行

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

Vue.mixin 方法是在 initGlobalAPI 方法中给 Vue 构造函数赋值上的,具体代码为:

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 接收一个 Object,然后调用 mergeOptions 合并配置。

二: 执行 new Vue(options),调用 this._init(options),然后就执行 _init 函数里的如下逻辑来合并 options

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

mergeOptions 方法实际上就是传入两个 Object,然后做合并。resolveConstructorOptions(vm.constructor) 暂时不考虑,在这里它只是返回了 vm.constructor.options,相当于是 Vue.options,那么这个值是什么,其实它也是在 initGlobalAPI 方法执行的时候定义的,代码如下:

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 = {
  components: {
    KeepAlive: {
      name: "keep-alive",
      // ...
    },
  },
  created: [],
  directives: {},
  filters: {},
  _base: Vue,
};

另外通过 extend(Vue.options.components, builtInComponents)Vue 内置的组件(如 KeepAlive )扩展到 Vue.options.components 中。这也是我们后面使用 KeepAlive 不用注册的原因,关于组件注册后面再看。

回到 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;
  }
  // 标准化 props、inject、directive 选项
  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);
    }
  }
  // 合并字段,child 选项将覆盖子选项
  function mergeField(key) {
    // strats 或者 defaultStrat 是个合并策略,即到底用父的还是用子的
    const strat = strats[key] || defaultStrat;
    // 优先使用 child 子选项的值
    options[key] = strat(parent[key], child[key], vm, key);
  }
  return options;
}

mergeOptions 主要做了几件事:

  1. 递归的把 extendsmixins 合并到 parent
  2. 遍历 parent,调用 mergeField
  3. 遍历 child,如果 keyparent 上不存在,则调用 mergeField

执行 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;
}

[
  "beforeCreate",
  "created",
  "beforeMount",
  "mounted",
  "beforeUpdate",
  "updated",
  "beforeDestroy",
  "destroyed",
  "activated",
  "deactivated",
  "errorCaptured", // 2.5.0 新增的钩子
].forEach((hook) => {
  strats[hook] = mergeHook;
});

这里定义了 Vue 中的所有生命周期钩子函数,mergeHook 执行的结果就是把 childparent 中定义的比如 created(){} 合并为一个数组并返回。 其他策略定义都可以在 src/core/util/options.js 看到,这里不一一细看了。

因此,我们最后执行完合并配置后:

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

vm.$options 的值应该为:

vm.$options = {
  components: {}
  created: [
    function created() {
      console.log('parent created')
    }
  ]
  directives: {}
  el: "#app"
  filters: {}
  render: function render(h) {}
  _base: function Vue(options) {}
}

组件调用场景

我们在之前了解到组件的构造函数是通过 Vue.extend 继承自 Vue 的,具体代码定义在 src/core/global-api/extend.js 中:

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 就是我们定义的组件对象 let childComp = { //... },他会和 Vue.options 合并到 Sub.options 中。 接下来我们会看一下组件初始化的过程,定义在 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);
}

在这里 vnode.componentOptions.Ctor = Sub,所以执行的下一步就是调用 _init(options) 方法。因为 _isComponent = true,所以合并配置走到 initInternalComponent(vm, options)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;
  }
}

initInternalComponent 主要做了下面几件事:

  1. 执行 const opts = vm.$options = Object.create(vm.constructor.options),也就是 vm.$options = Object.create(Sub.options)
  2. 把实例化子组件传入的子组件 父 VNode 实例 parentVnode、子组件的 父 Vue 实例 parent 保存到 vm.$options
  3. 保留了 parentVnode 配置中的如 propsData 等其它的属性

并没有像 mergeOptions 一样递归、合并策略等逻辑。

因此执行完 initInternalComponent 之后,vm.$options 的值应该为:

vm.$options = {
  parent: Vue, // 父Vue实例
  propsData: undefined,
  _componentTag: undefined,
  _parentListeners: undefined,
  _renderChildren: undefined,
  _parentVnode: VNode, // 父VNode实例
  __proto__: {
    components: {},
    created: [
      function created() {
        console.log("parent created");
      },
      function created() {
        console.log("child created");
      },
    ],
    data() {
      return {
        msg: "Hello Vue",
      };
    },
    directives: {},
    filters: {},
    mounted: [
      function mounted() {
        console.log("child mounted");
      },
    ],
    template: "<div>{{msg}}</div>",
    _Ctor: {},
    _base: function Vue(options) {
      //...
    },
  },
};

总结

09.组件化-合并配置.png

本文总结了两种初始化 Vue 的合并配置过程,我们只要知道对于 options 有两种合并方式,组件通过调用 initInternalComponent 比外部调用 Vue 初始化的过程要快,合并完的结果存放在 vm.$options 中。

纵观一些库、框架的设计几乎都是类似的,自身定义了一些默认配置,同时又可以在初始化阶段传入一些定义配置,然后去 merge 默认配置,来达到定制化不同需求的目的。只不过在 Vue 的场景下,会对 merge 的过程做一些精细化控制,虽然我们在开发自己的 JSSDK 的时候并没有 Vue 这么复杂,但这个设计思想是值得我们借鉴的。 - Vue.js 技术揭秘

源码分析 GitHub 地址

参考:Vue.js 技术揭秘