Vue2源码学习笔记之选项合并的规范化

我们在使用 new 创建 Vue 的实例对象时,必定传入一个参数对象 options 进去,这个参数对象其实就是 Vue 的选项 options 。比如在 Vue 项目的入口文件中。

目录
  1. resolveConstructorOptions
  2. mergeOptions
    1. checkComponents
    2. functional options
    3. normalizeFn
    4. 规范化extends和mixins
  3. resolveConstructorOptions
  4. mergeOptions
    1. checkComponents
    2. functional options
    3. normalizeFn
    4. 规范化extends和mixins
  5. 参考资料

我们在使用new创建 Vue 的实例对象时,必定传入一个参数对象options进去,这个参数对象其实就是 Vue 的选项options。比如在 Vue 项目的入口文件中。

// main.js
const vm = new Vue({
  el: "#app",
  router,
  store,
  render: (h) => h(App),
});

另外,创建每一个组件时,在script中也会导出一个对象,例如:

<template>
  <!-- 省略模板代码 -->
</template>
 
<script>
  export default {
    props: {},
    data() {
      return {};
    },
    computed: {},
    methods: {},
    // ...
  };
</script>

这个对象也是用户输入的options,项目入口文件有options,组件也有options,再加上 Vue 本身也自带options,这么多的options看起来非常乱,所以需要合并选项。

在初始化实例的方法_init中,使用了mergeOptions方法合并选项。

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 || {}, // ! 用户传入的配置
    vm // !  Vue 实例对象本身
  );
}

使用mergeOptions方法传入了三个参数,分别是默认选项、用户选项以及实例对象本身。

第一个参数是默认选项,通过调用resolveConstructorOptions函数时传入vm.constructor参数来获取的。

resolveConstructorOptions#

先看下resolveConstructorOptions函数的代码,从函数名可以看出这个函数的作用应该是解析构造函数的选项的。

export function resolveConstructorOptions(Ctor: Class<Component>) {
  let options = Ctor.options;
  if (Ctor.super) {
    // ...
  }
  return options;
}

这个函数传入的参数是vm.constructor,即实例对象的构造函数,也就是Vue

先声明变量options存储构造函数的options,然后下面是一个条件语言,如果存在Ctor.super属性,就会执行后面的代码。这个super的属性到底是什么呢?

如果我们使用了 Vue 的全局方法extend创建一个子类,再通过这个子类创建一个实例对象,那么这个实例对象的constructor就是这个子类。

const Sub = Vue.extend(); // 子类
const s = new Sub(); // 子类生成的实例对象

通过extend创建的子类会有一个特性的属性super,它的值一般就是 Vue 的构造函数。

因为这里是 Vue 的实例初始化的过程,所以传入的参数Ctor就是 Vue 的构造函数,而不是子类,也不可能有super属性,所以函数的if的代码块是不会执行的。

在实例对象创建的过程中,这个函数就可以简化为:

export function resolveConstructorOptions(Ctor: Class<Component>) {
  let options = Ctor.options;
  return options;
}

函数最后返回Ctor.options,即Vue的默认选项options。它现在应该是下面这个样子。

Vue.options = {
	components: {
		KeepAlive
		Transition,
    	TransitionGroup
	},
	directives:{
	    model,
        show
	},
	filters: Object.create(null),
	_base: Vue
}

这些选项都是在扩展 Vue 的构造函数和平台化包装时添加到 Vue 的构造函数的options属性中的。

mergeOptions函数传入的第二个参数options是用户输入的,在 Vue 项目的入口文件中,使用new创建 Vue 的实例并传入选项配置,传入的选项可能是下面这个样子。

{
  el: '#app',
  data: {
    a: 1
  }
}

mergeOptions函数传入的最后一个参数vm,这是实例对象本身。

mergeOptions#

checkComponents#

分析函数调用时的参数后,接下来看下mergeOptions函数的代码,在core/util/options.js文件中。

函数代码比较长,先看前面的代码。

export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== "production") {
    checkComponents(child);
  }
}

先查看函数的参数,参数分别是parentchildvm。在实例初始化调用函数的,parent就是默认选项配置,child是用户输入的选项配置,vm是实例对象。

再看函数体代码,在非生产环境中,会调用checkComponents函数检查一下child选项,这里主要校验选项的components属性中的各个组件名。

我们在组件中引入另外一个组件时,都需要注册引入的这个组件,注册是把组件放入到components选项中。

import CompA from "../components/CompA";
export default {
  components: { CompA },
  // ...
};

下面看下checkComponents函数的代码。

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

函数中遍历components选项中的元素,再使用validateComponentName函数检查下选项中的name是否符合要求。name就是components对象中key的值。

export function validateComponentName(name: string) {
  if (
    !new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)
  ) {
    warn(
      'Invalid component name: "' +
        name +
        '". Component names ' +
        "should conform to valid custom element name in html5 specification."
    );
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      "Do not use built-in or reserved HTML elements as component " +
        "id: " +
        name
    );
  }
}

使用了两个if来判断name的值是否符合要求,也就是需要同时符合这两个条件才不会报错。

  1. 满足正则表达式new RegExp(^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$)

  2. 满足条件isBuiltInTag(name) || config.isReservedTag(name)

对于第一条,name的值的开头必须由字符组成,后面可以是-符或者.符或者数字以及特殊字符。

对于第二条,name的值不能是 Vue 内置的组件名比如slotcomponent或者是保留的 HTML 标签名。

isBuiltInTag函数的代码如下:

export const isBuiltInTag = makeMap("slot,component", true);

isReservedTag函数的代码如下

export const isReservedTag = (tag: string): ?boolean => {
  return isHTMLTag(tag) || isSVG(tag);
};

functional options#

继续看mergeOptions函数下面的代码。

// ! 当 child 是函数时(Vue.extend 创造的子类),获取其静态属性 options
if (typeof child === "function") {
  child = child.options;
}

如果用户的选项是函数类型的,就获取它的options属性。

什么时候选项是一个函数呢?另外,什么函数会有属性options呢?

Vue 的构造函数就有这个options属性,另外通过Vue.extend创建的子类也可能会有options属性。

也就是说对于child这个参数可以传入一个 Vue 的构造函数,也可以传入通过Vue.extend的创建的子类。

normalizeFn#

继续看下面的代码。

// ! 有些类型的选项比较灵活多变,需要先规范化后,再合并
normalizeProps(child, vm); // ! 规范化 props 类型
normalizeInject(child, vm); // ! 规范化 inject 类型
normalizeDirectives(child); // ! 规范化 指令

这里使用了三个函数去规范化选项,分别规范propsinjectdirective选项。

为什么要去规范化这些选项呢?以props选项为例,它的值非常灵活,即可以是数组,也可以是对象。另外,对象里面的值可以是一个表示数据类型的值,也可以是一个普通对象。

示例代码如下:

// 数组类型
props: ['name', 'age']
 
// 对象类型
props: {
  name: String, // 默认值为类型
  age: {
    type: Number,
    default: 18
  }
}

Vue 为使用者提供这些灵活的接口,对于我们来说是好事,但对于 Vue 来说却不是,因为需要把不同类型或者格式的数据规范成统一的数据类型和格式,这样后面才能更好的合并选项。

下面分别查看每个规范函数的代码,这些函数都是在core/util/options.js文件中。

normalizeProps

查看normalizeProps函数的代码。

function normalizeProps(options: Object, vm: ?Component) {
  const props = options.props;
  if (!props) return;
  const res = {};
  let i, val, name;
  // ! 规范数组类型的 props
  if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i];
      if (typeof val === "string") {
        name = camelize(val);
        res[name] = { type: null }; // !不清楚类型统一设置为 null
      } else if (process.env.NODE_ENV !== "production") {
        warn("props must be strings when using array syntax.");
      }
    }
    // ! 规范对象类型的 props
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key];
      name = camelize(key);
      res[name] = isPlainObject(val) ? val : { type: val };
    }
  } else if (process.env.NODE_ENV !== "production") {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
        `but got ${toRawType(props)}.`,
      vm
    );
  }
  options.props = res; // ! 最终都转换成对象类型的值
}

normalizeProps函数是用来规范props选项的,代码逻辑并不难。因为选项props的值只能有两种类型,使用if语句分别判断这两种类型即可,如果它的值都不是这两种类型,在非生产环境中会报错。

先声明一个变量res存储规范后的值,它的初始值是一个对象,然后再进行规范。

props的值是数组类型时,遍历数组,然后以数组的每个元素作为key{type: null}作为值,组成键值对的形式存储在前面声明的变量res中。另外还需要判断数组里面的元素的值必须是字符串类型,否则在非生产环境中会报错。

示例代码:

// 数组类型
props: ['name', 'age']
 
// 规范后的数组类型
props: {
  name: { type: null },
  age: { type: null }
}

props的值是对象时,遍历对象,使用变量val存储对象中原有的值,如果值是一个普通的对象时,直接赋值为原来的值。如果不是,就把这个值转换成类型的值{type: val}

示例代码:

// 对象类型
props: {
  name: String,
  age: { type: Number, default: 18 }
}
 
// 规范后的对象类型
props: {
  name: { type: String },
  age: { type: Number, default: 18 }
}

在上面的处理过程中使用了camelize函数把每个连字符-形式的key转换成驼峰的形式。

小结,不管props选项的值是数组类型还是对象类型,最后都会转换成对象类型,然后对象里的值也是一个对象。

normalizeInject

查看normalizeInject函数的代码。

function normalizeInject(options: Object, vm: ?Component) {
  const inject = options.inject;
  if (!inject) return;
  const normalized = (options.inject = {});
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] };
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key];
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val };
    }
  } else if (process.env.NODE_ENV !== "production") {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
        `but got ${toRawType(inject)}.`,
      vm
    );
  }
}
s;

normalizeInject函数是用来规范inject选项的,代码逻辑也不难。选项inject的用法和props有点相似,它的值可以是数组类型也可以是对象类型。如果不是这两种类型,在非生产环境中会报错。

inject的值是数组类型时,遍历数组,然后转换成键值对的对象类型,其中键和值都是原来的数组元素。

示例代码:

// 数组类型
inject: ['data1', 'data2']
 
// 规范后的数组类型
inject: {
  data1: { from: 'data1'},
	data2: { from: 'data2'}
}

inject的值为对象类型时,遍历对象。如果对象里的值是普通对象时,在对象里面设置from的值为原来key的值,然后和原来的对象进行合并。如果对象里的值是字符串时,把它变成一个对象,在设置对象里面的from的值为原来的字符串类型的值,原来对象的key不会改变。

示例代码:

// 对象类型
inject: {
  data1, // 相当于 data1: data1
  d2: 'data2',
  data3: { someProperty: 'someValue' }
}
 
// 规范后的对象类型
inject: {
  data1: { from: 'data1' },
  d2: { from: 'data2'},
  data3: { from: 'data3', someProperty: 'someValue' }
}

小结,不管inject选项的值是数组类型还是对象类型,最后都会转换成对象类型,然后对象里的值也是一个对象。

normalizeDirectives

查看normalizeDirectives函数的代码。

function normalizeDirectives(options: Object) {
  const dirs = options.directives;
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key];
      if (typeof def === "function") {
        dirs[key] = { bind: def, update: def };
      }
    }
  }
}

normalizeDirectives函数是用来规范directives选项的。directives的值只能是对象类型,但是对象里面的值可以是函数类型,也可以是对象类型。如果它的值是一个函数,就把它转换成对象,并赋值成为对象的bindupdate属性,如果它的值是一个对象,则不用转换。

示例代码:

directives: {
  test1: {
    bind: function () {
      console.log('v-test1')
    }
  },
  test2: function () {
    console.log('v-test2')
  }
}
 
// 规范后
directives: {
  test1: {
    bind: function () {
      console.log('v-test1')
    }
  },
  test2: {
    bind: function () {
    	console.log('v-test2')
  	},
    update: function () {
    	console.log('v-test2')
  	}
  }
}

规范化extendsmixins#

继续看mergeOptions函数后面的代码。

if (!child._base) {
  if (child.extends) {
    parent = mergeOptions(parent, child.extends, vm); // ! 合并 extends
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm); // ! 合并 mixins
    }
  }
}

上面的代码是处理options中的extends选项和mixins选项的,这两个选项都和组件的代码复用有关。

对于这两个选项,都是递归调用mergeOptions来处理,并且把默认的parent选项 、用户输入的extends或者mixins选项进行合并,生成一个全新的选项作为默认选项parent

extends选项的值是不管是对象类型还是函数类型,都可以直接和parent选项进行合并,因为前面已经对函数类型的值进行了处理。

示例代码:

// 对象类型
extends: {
  data() {
    return {
      // ...
    }
  },
  created() {
    // ...
  }
}
 
// 函数类型,使用 Vue.extend 生成子类
const Ctor = Vue.extend({
  data() {
    return {
      // ...
    }
  },
  created() {
    // ...
  }
})
extends: Ctor

mixins选项的值必须是数组类型,所以需要遍历数组,而数组中的元素mixin必须是对象类型,所以可以按照顺序一个个和parent进行合并。这里注意一下优先级,数组中排序靠后的mixin中的配置会覆盖前面的mixin的配置。

示例代码

userMixin: {
	data() {
		// ...
	}
},
playMixin: {
	data() {
		// ...
	},
	methods: {
	 // ...
	}
}
 
mixins: [userMixin, playMixin]

另外,创建每一个组件时,在script中也会导出一个对象,例如:

<template>
  <!-- 省略模板代码 -->
</template>
 
<script>
  export default {
    props: {},
    data() {
      return {};
    },
    computed: {},
    methods: {},
    // ...
  };
</script>

这个对象也是用户输入的options,项目入口文件有options,组件也有options,再加上 Vue 本身也自带options,这么多的options看起来非常乱,所以需要合并选项。

在初始化实例的方法_init中,使用了mergeOptions方法合并选项。

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 || {}, // ! 用户传入的配置
    vm // !  Vue 实例对象本身
  );
}

使用mergeOptions方法传入了三个参数,分别是默认选项、用户选项以及实例对象本身。

第一个参数是默认选项,通过调用resolveConstructorOptions函数时传入vm.constructor参数来获取的。

resolveConstructorOptions#

先看下resolveConstructorOptions函数的代码,从函数名可以看出这个函数的作用应该是解析构造函数的选项的。

export function resolveConstructorOptions(Ctor: Class<Component>) {
  let options = Ctor.options;
  if (Ctor.super) {
    // ...
  }
  return options;
}

这个函数传入的参数是vm.constructor,即实例对象的构造函数,也就是Vue

先声明变量options存储构造函数的options,然后下面是一个条件语言,如果存在Ctor.super属性,就会执行后面的代码。这个super的属性到底是什么呢?

如果我们使用了 Vue 的全局方法extend创建一个子类,再通过这个子类创建一个实例对象,那么这个实例对象的constructor就是这个子类。

const Sub = Vue.extend(); // 子类
const s = new Sub(); // 子类生成的实例对象

通过extend创建的子类会有一个特性的属性super,它的值一般就是 Vue 的构造函数。

因为这里是 Vue 的实例初始化的过程,所以传入的参数Ctor就是 Vue 的构造函数,而不是子类,也不可能有super属性,所以函数的if的代码块是不会执行的。

在实例对象创建的过程中,这个函数就可以简化为:

export function resolveConstructorOptions(Ctor: Class<Component>) {
  let options = Ctor.options;
  return options;
}

函数最后返回Ctor.options,即Vue的默认选项options。它现在应该是下面这个样子。

Vue.options = {
	components: {
		KeepAlive
		Transition,
    	TransitionGroup
	},
	directives:{
	    model,
        show
	},
	filters: Object.create(null),
	_base: Vue
}

这些选项都是在扩展 Vue 的构造函数和平台化包装时添加到 Vue 的构造函数的options属性中的。

mergeOptions函数传入的第二个参数options是用户输入的,在 Vue 项目的入口文件中,使用new创建 Vue 的实例并传入选项配置,传入的选项可能是下面这个样子。

{
  el: '#app',
  data: {
    a: 1
  }
}

mergeOptions函数传入的最后一个参数vm,这是实例对象本身。

mergeOptions#

checkComponents#

分析函数调用时的参数后,接下来看下mergeOptions函数的代码,在core/util/options.js文件中

函数代码比较长,先看前面的代码。

export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== "production") {
    checkComponents(child);
  }
}

先查看函数的参数,参数分别是parentchildvm。在实例初始化调用函数的,parent就是默认选项配置,child是用户输入的选项配置,vm是实例对象。

再看函数体代码,在开发环境中,会调用checkComponents函数检查一下child选项,这里主要校验选项的components属性中的各个组件名。

我们在组件中引入另外一个组件时,都需要注册引入的这个组件,注册是把组件放入到components选项中。

import CompA from "../components/CompA";
export default {
  components: { CompA },
  // ...
};

下面看下checkComponents函数的代码。

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

函数中遍历components选项中的元素,再使用validateComponentName函数检查下选项中的name是否符合要求。name就是components对象中key的值。

export function validateComponentName(name: string) {
  if (
    !new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)
  ) {
    warn(
      'Invalid component name: "' +
        name +
        '". Component names ' +
        "should conform to valid custom element name in html5 specification."
    );
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      "Do not use built-in or reserved HTML elements as component " +
        "id: " +
        name
    );
  }
}

使用了两个if来判断name的值是否符合要求,也就是需要同时符合这两个条件才不会报错。

  1. 满足正则表达式new RegExp(^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$)

  2. 满足条件isBuiltInTag(name) || config.isReservedTag(name)

对于第一条,name的值的开头必须由字符组成,后面可以是-符或者.符或者数字以及特殊字符。

对于第二条,name的值不能是 Vue 内置的组件名比如slotcomponent或者是保留的 HTML 标签名。

isBuiltInTag函数的代码如下:

export const isBuiltInTag = makeMap("slot,component", true);

isReservedTag函数的代码如下:

export const isReservedTag = (tag: string): ?boolean => {
  return isHTMLTag(tag) || isSVG(tag);
};

functional options#

继续看mergeOptions函数下面的代码。

// ! 当 child 是函数时(Vue.extend 创造的子类),获取其静态属性 options
if (typeof child === "function") {
  child = child.options;
}

如果用户的选项是函数类型的,就获取它的options属性。

什么时候选项是一个函数呢?另外,什么函数会有属性options呢?

Vue 的构造函数就有这个options属性,另外通过Vue.extend创建的子类也可能会有options属性。

也就是说对于child这个参数可以传入一个 Vue 的构造函数,也可以传入通过Vue.extend的创建的子类。

normalizeFn#

接下来继续看代码。

// ! 有些类型的选项比较灵活多变,需要先规范化后,再合并
normalizeProps(child, vm); // ! 规范化 props 类型
normalizeInject(child, vm); // ! 规范化 inject 类型
normalizeDirectives(child); // ! 规范化 指令

这里使用了三个函数去规范化选项,分别规范propsinjectdirective选项。

为什么要去规范化这些选项呢?以props选项为例,它的值非常灵活,即可以是数组,也可以是对象。另外,对象里面的值可以是一个表示数据类型的值,也可以是一个普通对象。

示例代码如下:

// 数组类型
props: ['name', 'age']
 
// 对象类型
props: {
  name: String, // 默认值为类型
  age: {
    type: Number,
    default: 18
  }
}

Vue 为使用者提供这些灵活的接口,对于我们来说是好事,但对于 Vue 来说却不是,因为需要把不同类型或者格式的数据规范成统一的数据类型和格式,这样后面才能更好的合并选项。

下面分别查看每个规范函数的代码,这些函数都是在core/util/options.js文件中。

normalizeProps

查看normalizeProps函数的代码。

function normalizeProps(options: Object, vm: ?Component) {
  const props = options.props;
  if (!props) return;
  const res = {};
  let i, val, name;
  // ! 规范数组类型的 props
  if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i];
      if (typeof val === "string") {
        name = camelize(val);
        res[name] = { type: null }; // !不清楚类型统一设置为 null
      } else if (process.env.NODE_ENV !== "production") {
        warn("props must be strings when using array syntax.");
      }
    }
    // ! 规范对象类型的 props
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key];
      name = camelize(key);
      res[name] = isPlainObject(val) ? val : { type: val };
    }
  } else if (process.env.NODE_ENV !== "production") {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
        `but got ${toRawType(props)}.`,
      vm
    );
  }
  options.props = res; // ! 最终都转换成对象类型的值
}

normalizeProps函数是用来规范props选项的,代码逻辑并不难。因为选项props的值只能有两种类型,使用if语句分别判断这两种类型即可,如果它的值都不是这两种类型,在开发环境中会报错。

先声明一个变量res存储规范后的值,它的初始值是一个对象,然后再进行规范。

props的值是数组类型时,遍历数组,然后以数组的每个元素作为key{type: null}作为值,组成键值对的形式存储在前面声明的变量res中。另外还需要判断数组里面的元素的值必须是字符串类型,否则在开发环境中会报错。

示例代码:

// 数组类型
props: ['name', 'age']
 
// 规范后的数组类型
props: {
  name: { type: null },
  age: { type: null }
}

props的值是对象时,遍历对象,使用变量val存储对象中原有的值,如果值是一个普通的对象时,直接赋值为原来的值。如果不是,就把这个值转换成类型的值{type: val}

示例代码:

// 对象类型
props: {
  name: String,
  age: { type: Number, default: 18 }
}
 
// 规范后的对象类型
props: {
  name: { type: String },
  age: { type: Number, default: 18 }
}

在上面的处理过程中使用了camelize函数把每个连字符-形式的key转换成驼峰的形式。

小结,不管props选项的值是数组类型还是对象类型,最后都会转换成对象类型,然后对象里的值也是一个对象。

normalizeInject

查看normalizeInject函数的代码。

function normalizeInject(options: Object, vm: ?Component) {
  const inject = options.inject;
  if (!inject) return;
  const normalized = (options.inject = {});
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] };
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key];
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val };
    }
  } else if (process.env.NODE_ENV !== "production") {
    warn(
      `Invalid value for option "inject": expected an Array or an Object, ` +
        `but got ${toRawType(inject)}.`,
      vm
    );
  }
}
s;

normalizeInject函数是用来规范inject选项的,代码逻辑也不难。选项inject的用法和props有点相似,它的值可以是数组类型也可以是对象类型。如果不是这两种类型,在开发环境中会报错。

inject的值是数组类型时,遍历数组,然后转换成键值对的对象类型,其中键和值都是原来的数组元素。

示例代码:

// 数组类型
inject: ['data1', 'data2']
 
// 规范后的数组类型
inject: {
  data1: { from: 'data1' },
	data2: { from: 'data2' }
}

inject的值为对象类型时,遍历对象。如果对象里的值是普通对象时,在对象里面设置from的值为原来key的值,然后和原来的对象进行合并。如果对象里的值是字符串时,把它变成一个对象,在设置对象里面的from的值为原来的字符串类型的值,原来对象的key不会改变。

示例代码:

// 对象类型
inject: {
  data1, // 相当于 data1: data1
  d2: 'data2',
  data3: { someProperty: 'someValue' }
}
 
// 规范后的对象类型
inject: {
  data1: { from: 'data1' },
  d2: { from: 'data2'},
  data3: { from: 'data3', someProperty: 'someValue' }
}

小结,不管inject选项的值是数组类型还是对象类型,最后都会转换成对象类型,然后对象里的值也是一个对象。

normalizeDirectives

查看normalizeDirectives函数的代码。

function normalizeDirectives(options: Object) {
  const dirs = options.directives;
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key];
      if (typeof def === "function") {
        dirs[key] = { bind: def, update: def };
      }
    }
  }
}

normalizeDirectives函数是用来规范directives选项的。directives的值只能是对象类型,但是对象里面的值可以是函数类型,也可以是对象类型。如果它的值是一个函数,就把它转换成对象,并赋值成为对象的bindupdate属性,如果它的值是一个对象,则不用转换。

示例代码:

directives: {
  test1: {
    bind: function () {
      console.log('v-test1')
    }
  },
  test2: function () {
    console.log('v-test2')
  }
}
 
// 规范后
directives: {
  test1: {
    bind: function () {
      console.log('v-test1')
    }
  },
  test2: {
    bind: function () {
    	console.log('v-test2')
  	},
    update: function () {
    	console.log('v-test2')
  	}
  }
}

规范化extendsmixins#

继续看mergeOptions函数后面的代码。

if (!child._base) {
  if (child.extends) {
    parent = mergeOptions(parent, child.extends, vm); // ! 合并 extends
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm); // ! 合并 mixins
    }
  }
}

上面的代码是处理options中的extends选项和mixins选项的,这两个选项都和组件的代码复用有关。

对于这两个选项,都是递归调用mergeOptions来处理,并且把默认的parent选项 、用户输入的extends或者mixins选项进行合并,生成一个全新的选项作为默认选项parent

extends选项的值是不管是对象类型还是函数类型,都可以直接和parent选项进行合并,因为前面已经对函数类型的值进行了处理。

示例代码:

// 对象类型
extends: {
  data() {
    return {
      // ...
    }
  },
  created() {
    // ...
  }
}
 
// 函数类型,使用 Vue.extend 生成子类
const Ctor = Vue.extend({
  data() {
    return {
      // ...
    }
  },
  created() {
    // ...
  }
})
extends: Ctor

mixins选项的值必须是数组类型,所以需要遍历数组,而数组中的元素mixin必须是对象类型,所以可以按照顺序一个个和parent进行合并。这里注意一下优先级,数组中排序靠后的mixin中的配置会覆盖前面的mixin的配置。

示例代码:

userMixin: {
	data() {
		// ...
	}
},
playMixin: {
	data() {
		// ...
	},
	methods: {
	 // ...
	}
}
 
mixins: [userMixin, playMixin]

参考资料#