我们在使用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);
}
}先查看函数的参数,参数分别是parent、child和vm。在实例初始化调用函数的,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的值是否符合要求,也就是需要同时符合这两个条件才不会报错。
-
满足正则表达式
new RegExp(^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$) -
满足条件
isBuiltInTag(name) || config.isReservedTag(name)
对于第一条,name的值的开头必须由字符组成,后面可以是-符或者.符或者数字以及特殊字符。
对于第二条,name的值不能是 Vue 内置的组件名比如slot、component或者是保留的 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); // ! 规范化 指令这里使用了三个函数去规范化选项,分别规范props、inject和directive选项。
为什么要去规范化这些选项呢?以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的值只能是对象类型,但是对象里面的值可以是函数类型,也可以是对象类型。如果它的值是一个函数,就把它转换成对象,并赋值成为对象的bind和update属性,如果它的值是一个对象,则不用转换。
示例代码:
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')
}
}
}规范化extends和mixins#
继续看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);
}
}先查看函数的参数,参数分别是parent、child和vm。在实例初始化调用函数的,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的值是否符合要求,也就是需要同时符合这两个条件才不会报错。
-
满足正则表达式
new RegExp(^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$) -
满足条件
isBuiltInTag(name) || config.isReservedTag(name)
对于第一条,name的值的开头必须由字符组成,后面可以是-符或者.符或者数字以及特殊字符。
对于第二条,name的值不能是 Vue 内置的组件名比如slot、component或者是保留的 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); // ! 规范化 指令这里使用了三个函数去规范化选项,分别规范props、inject和directive选项。
为什么要去规范化这些选项呢?以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的值只能是对象类型,但是对象里面的值可以是函数类型,也可以是对象类型。如果它的值是一个函数,就把它转换成对象,并赋值成为对象的bind和update属性,如果它的值是一个对象,则不用转换。
示例代码:
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')
}
}
}规范化extends和mixins#
继续看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]