Vue 选项的合并
上一章节我们了解了 Vue
对选项的规范化,而接下来才是真正的合并阶段,我们继续看 mergeOptions
函数的代码,接下来的一段代码如下:
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
这段代码的第一句和最后一句说明了 mergeOptions
函数的的确确返回了一个新的对象,因为第一句代码定义了一个常量 options
,而最后一句代码将其返回,所以我们自然可以预估到中间的代码是在充实 options
常量,而 options
常量就应该是最终合并之后的选项,我们看看它是怎么产生的。
首先我们明确一下代码结构,这里有两个 for in
循环以及一个名字叫 mergeField
的函数,而且我们可以发现这两个 for in
循环中都调用了 mergeField
函数。我们先看第一段 for in
代码:
for (key in parent) {
mergeField(key)
}
这段 for in
用来遍历 parent
,并且将 parent
对象的键作为参数传递给 mergeField
函数,大家应该知道这里的 key
是什么,假如 parent
就是 Vue.options
:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives:{
model,
show
},
filters: Object.create(null),
_base: Vue
}
那么 key
就应该分别是:components
、directives
、filters
以及 _base
,除了 _base
其他的字段都可以理解为是 Vue
提供的选项的名字。
而第二段 for in
代码:
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
其遍历的是 child
对象,并且多了一个判断:
if (!hasOwn(parent, key))
其中 hasOwn
函数来自于 shared/util.js
文件,可以在 shared/util.js 文件工具方法全解 中查看其详解,其作用是用来判断一个属性是否是对象自身的属性(不包括原型上的)。所以这个判断语句的意思是,如果 child
对象的键也在 parent
上出现,那么就不要再调用 mergeField
了,因为在上一个 for in
循环中已经调用过了,这就避免了重复调用。
总之这两个 for in
循环的目的就是使用在 parent
或者 child
对象中出现的 key(即选项的名字)
作为参数调用 mergeField
函数,真正合并的操作实际在 mergeField
函数中。
mergeField
代码如下:
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
mergeField
函数只有两句代码,第一句代码定义了一个常量 strat
,它的值是通过指定的 key
访问 strats
对象得到的,而当访问的属性不存在时,则使用 defaultStrat
作为值。
这里我们就要明确了,strats
是什么?想弄明白这个问题,我们需要从整体角度去看一下 options.js
文件,首先看文件顶部的一堆 import
语句下的第一句代码:
/**
* 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
变量,且它是一个常量,这个常量的值为 config.optionMergeStrategies
,这个 config
对象是全局配置对象,来自于 core/config.js
文件,此时 config.optionMergeStrategies
还只是一个空的对象。注意一下这里的一段注释:选项覆盖策略是处理如何将父选项值和子选项值合并到最终值的函数。也就是说 config.optionMergeStrategies
是一个合并选项的策略对象,这个对象下包含很多函数,这些函数就可以认为是合并特定选项的策略。这样不同的选项使用不同的合并策略,如果你使用自定义选项,那么你也可以自定义该选项的合并策略,只需要在 Vue.config.optionMergeStrategies
对象上添加与自定义选项同名的函数就行。而这就是 Vue
文档中提过的全局配置:optionMergeStrategies。
选项 el、propsData 的合并策略
那么接下来我们就看看这个选项合并策略对象都有哪些策略,首先是下面这段代码:
/**
* Options with restrictions
*/
if (process.env.NODE_ENV !== 'production') {
strats.el = strats.propsData = function (parent, child, vm, key) {
if (!vm) {
warn(
`option "${key}" can only be used during instance ` +
'creation with the `new` keyword.'
)
}
return defaultStrat(parent, child)
}
}
非生产环境下在 strats
策略对象上添加两个策略(两个属性)分别是 el
和 propsData
,且这两个属性的值是一个函数。通过这两个属性的名字可知,这两个策略函数是用来合并 el
选项和 propsData
选项的。与其说“合并”不如说“处理”,因为其本质上并没有做什么合并工作。那么我们看看这个策略函数的具体内容,了解一下它是怎么处理 el
和 propsData
选项的。
首先是一段 if
判断分支,判断是否有传递 vm
参数:
if (!vm) {
warn(
`option "${key}" can only be used during instance ` +
'creation with the `new` keyword.'
)
}
如果没有传递这个参数,那么便会给你一个警告,提示你 el
选项或者 propsData
选项只能在使用 new
操作符创建实例的时候可用。比如下面的代码:
// 子组件
var ChildComponent = {
el: '#app2',
created: function () {
console.log('child component created')
}
}
// 父组件
new Vue({
el: '#app',
data: {
test: 1
},
components: {
ChildComponent
}
})
上面的代码中我们在父组件中使用 el
选项,这并没有什么问题,但是在子组件中也使用了 el
选项,这就会得到如上警告。这说明了一个问题,即在策略函数中如果拿不到 vm
参数,那说明处理的是子组件选项。所以问题来了,为什么通过判断 vm
是否存在,就能判断出是否是子组件呢?那首先我们要搞清楚策略函数中的 vm
参数是哪里来的。首先我们还是看一下 mergeField
函数:
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
函数体的第二句代码中在调用策略函数的时候,第三个参数 vm
就是我们在策略函数中使用的那个 vm
,那么这里的 vm
是谁呢?它实际上是从 mergeOptions
函数透传过来的,因为 mergeOptions
函数的第三个参数就是 vm
。我们知道在 _init
方法中调用 mergeOptions
函数时第三个参数就是当前 Vue
实例:
// _init 方法中调用 mergeOptions 函数,第三个参数是 Vue 实例
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
所以我们可以理解为:策略函数中的 vm
来自于 mergeOptions
函数的第三个参数。所以当调用 mergeOptions
函数且不传递第三个参数的时候,那么在策略函数中就拿不到 vm
参数。所以我们可以猜测到一件事,那就是 mergeOptions
函数除了在 _init
方法中被调用之外,还在其他地方被调用,且没有传递第三个参数。那么到底是在哪里被调用的呢?这里可以先明确地告诉大家,就是在 Vue.extend
方法中被调用的,大家可以打开 core/global-api/extend.js
文件找到 Vue.extend
方法,其中有这么一段代码:
Sub.options = mergeOptions(
Super.options,
extendOptions
)
可以发现,此时调用 mergeOptions
函数就没有传递第三个参数,也就是说通过 Vue.extend
创建子类的时候 mergeOptions
会被调用,此时策略函数就拿不到第三个参数。
所以现在就比较明朗了,在策略函数中通过判断是否存在 vm
就能够得知 mergeOptions
是在实例化时调用(使用 new
操作符走 _init
方法)还是在继承时调用(Vue.extend
),而子组件的实现方式就是通过实例化子类完成的,子类又是通过 Vue.extend
创造出来的,所以我们就能通过对 vm
的判断而得知是否是子组件了。
所以最终的结论就是:如果策略函数中拿不到 vm
参数,那么处理的就是子组件的选项,花了大量的口舌解释了策略函数中判断 vm
的意义,实际上这些解释是必要的。
我们接着看 strats.el
和 strats.propsData
策略函数的代码,在 if
判断分支下面,直接调用了 defaultStrat
函数并返回:
return defaultStrat(parent, child)
defaultStrat
函数就定义在 options.js
文件内,源码如下:
/**
* Default strategy.
*/
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
实际上 defaultStrat
函数就如同它的名字一样,它是一个默认的策略,当一个选项不需要特殊处理的时候就使用默认的合并策略,它的逻辑很简单:只要子选项不是 undefined
那么就是用子选项,否则使用父选项。
但是大家还要注意一点,strats.el
和 strats.propsData
这两个策略函数是只有在非生产环境才有的,在生产环境下访问这两个函数将会得到 undefined
,那这个时候 mergeField
函数的第一句代码就起作用了:
// 当一个选项没有对应的策略函数时,使用默认策略
const strat = strats[key] || defaultStrat
所以在生产环境将直接使用默认的策略函数 defaultStrat
来处理 el
和 propsData
这两个选项。
选项 data 的合并策略
下面我们接着按照顺序看 options.js
文件的代码,接下来定义了两个函数:mergeData
以及 mergeDataOrFn
,我们暂且不关注这两个函数的作用。暂且跳过继续看下面的代码,接下来的代码如下:
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
这段代码的作用是在 strats
策略对象上添加 data
策略函数,用来合并处理 data
选项。我们看看这个策略函数的内容,首先是一个判断分支:
if (!vm) {
...
}
与 el
和 propsData
这两个策略函数相同,先判断是否传递了 vm
这个参数,我们知道当没有 vm
参数时,说明处理的是子组件的选项,那我们就看看对于子组件的选项它是如何处理的,if
判断语句块内的代码如下:
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
首先判断是否传递了子组件的 data
选项(即:childVal
),并且检测 childVal
的类型是不是 function
,如果 childVal
的类型不是 function
则会给你一个警告,也就是说 childVal
应该是一个函数,如果不是函数会提示你 data
的类型必须是一个函数,这就是我们知道的:子组件中的 data
必须是一个返回对象的函数。如果不是函数,除了给你一段警告之外,会直接返回 parentVal
。
如果 childVal
是函数类型,那说明满足了子组件的 data
选项需要是一个函数的要求,那么就直接返回 mergeDataOrFn
函数的执行结果:
return mergeDataOrFn(parentVal, childVal)
上面的情况是在 strats.data
策略函数拿不到 vm
参数时的情况,如果拿到了 vm
参数,那么说明处理的选项不是子组件的选项,而是正常使用 new
操作符创建实例时的选项,这个时候则直接返回 mergeDataOrFn
的函数执行结果,但是会多透传一个参数 vm
:
return mergeDataOrFn(parentVal, childVal, vm)
通过上面的分析我们得知一件事,即 strats.data
策略函数无论合并处理的是子组件的选项还是非子组件的选项,其最终都是调用 mergeDataOrFn
函数进行处理的,并且以 mergeDataOrFn
函数的返回值作为策略函数的最终返回值。有一点不同的是在处理非子组件选项的时候所调用的 mergeDataOrFn
函数多传递了一个参数 vm
。所以接下来我们要做的事儿就是看看 mergeDataOrFn
的代码,看一看它的返回值是什么,因为它的返回值就等价于 strats.data
策略函数的返回值。mergeDataOrFn
函数的源码如下:
/**
* Data
*/
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
这个函数整体由 if
判断分支语句块组成,首先对 vm
进行判断,我们知道无论是子组件选项还是非子组件选项 strats.data
策略函数都是通过调用 mergeDataOrFn
函数来完成处理的,且处理非子组件选项的时候要比处理子组件选项时多传递了一个参数 vm
,这就使得 mergeDataOrFn
也能通过是否有 vm
来区分处理的是不是子组件选项。如果没有拿到 vm
参数的话,那说明处理的是子组件选项,程序会走 if
分支,实际上我们可以看到这里有段注释:
// in a Vue.extend merge, both should be functions
这段注释的意思是:选项是在调用 Vue.extend
函数时进行合并处理的,此时父子 data
选项都应该是函数。
这再次说明了,当拿不到 vm
这个参数的时候,合并操作是在 Vue.extend
中进行的,也就是在处理子组件的选项。而且此时 childVal
和 parentVal
都应该是函数,那么这里真的能保证 childVal
和 parentVal
都是函数了吗?其实是可以的,我们后面会讲到。
在这句注释的下面是这段代码:
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
我们看第一个 if
语句块,如果没有 childVal
,也就是说子组件的选项中没有 data
选项,那么直接返回 parentVal
,比如下面的代码:
Vue.extend({})
我们使用 Vue.extend
函数创建子类的时候传递的子组件选项是一个空对象,即没有 data
选项,那么此时 parentVal
实际上就是 Vue.options
,由于 Vue.options
上也没有 data
这个属性,所以压根就不会执行 strats.data
策略函数,也就更不会执行 mergeDataOrFn
函数,有的同学可能会问:既然都没有执行,那么这里的 return parentVal
是不是多余的?当然不多余,因为 parentVal
存在有值的情况。那么什么时候才会出现 childVal
不存在但是 parentVal
存在的情况呢?看下面的代码:
const Parent = Vue.extend({
data: function () {
return {
test: 1
}
}
})
const Child = Parent.extend({})
上面的代码中 Parent
类继承了 Vue
,而 Child
又继承了 Parent
,关键就在于我们使用 Parent.extend
创建 Child
子类的时候,对于 Child
类来讲,childVal
不存在,因为我们没有传递 data
选项,但是 parentVal
存在,即 Parent.options
下的 data
选项,那么 Parent.options
是哪里来的呢?实际就是 Vue.extend
函数内使用 mergeOptions
生成的,所以此时 parentVal
必定是个函数,因为 strats.data
策略函数在处理 data
选项后返回的始终是一个函数。
所以现在再看这段代码就清晰多了:
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
由于 childVal
和 parentVal
必定会有其一,否则便不会执行 strats.data
策略函数,所以上面判断的意思就是:如果没有子选项则使用父选项,没有父选项就直接使用子选项,且这两个选项都能保证是函数,如果父子选项同时存在,则代码继续进行,将执行下面的代码:
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
也就是说,当父子选项同时存在,那么就返回一个函数 mergedDataFn
,注意:此时代码运行就结束了,因为函数已经返回了(return
),至于 mergedDataFn
函数里面又返回了 mergeData
函数的执行结果这句代码目前还没有执行。
以上就是 strats.data
策略函数在处理子组件的 data
选项时所做的事,我们可以发现 mergeDataOrFn
函数在处理子组件选项时返回的总是一个函数,这也就间接导致 strats.data
策略函数在处理子组件选项时返回的也总是一个函数。
说完了处理子组件选项的情况,我们再看看处理非子组件选项的情况,也就是使用 new
操作符创建实例时的情况,此时程序直接执行 strats.data
函数的最后一句代码:
return mergeDataOrFn(parentVal, childVal, vm)
我们发现同样是调用 mergeDataOrFn
函数,只不过这个时候传递了 vm
参数,也就是说这将会执行 mergeDataOrFn
的 else
分支:
if (!vm) {
...
} else {
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
如果走了 else
分支的话那么就直接返回 mergedInstanceDataFn
函数,注意此时的 mergedInstanceDataFn
函数同样还没有执行,它是 mergeDataOrFn
函数的返回值,所以这再次说明了一个问题:mergeDataOrFn
函数永远返回一个函数。
也就是说,假如以我们的例子为例:
let v = new Vue({
el: '#app',
data: {
test: 1
}
})
我们的 data
选项在经过 mergeOptions
处理之后将变成一个函数,且根据我们的分析,它应该就是 mergedInstanceDataFn
函数,我们可以在控制台打印如下信息:
console.log(v.$options)
输出如下图:
我们可以发现 data
选项确实被 mergeOptions
处理成了一个函数,且当 data
选项为非子组件的选项时,该函数就是 mergedInstanceDataFn
。
一个简单的总结,现在我们了解到了一个事实,即 data
选项最终被 mergeOptions
函数处理成了一个函数,当合并处理的是子组件的选项时 data
函数可能是以下三者之一:
- 1、就是
data
本身,因为子组件的data
选项本身就是一个函数,即如下mergeDataOrFn
函数的代码段所示:
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
...
// 返回子组件的 data 选项本身
if (!parentVal) {
return childVal
}
...
} else {
...
}
}
- 2、父类的
data
选项,如下代码段所示::
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
...
// 返回父类的 data 选项
if (!childVal) {
return parentVal
}
...
} else {
...
}
}
- 3、
mergedDataFn
函数,如下代码段所示:
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
...
// 返回 mergedDataFn 函数
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
...
}
}
当合并处理的是非子组件的选项时 data
函数为 mergedInstanceDataFn
函数,如下代码段所示:
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
...
} else {
// 当合并处理的是非子组件的选项时 `data` 函数为 `mergedInstanceDataFn` 函数
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
所以这就是我们一直强调的:data
选项最终被处理为一个函数。但是根据我们之前的分析可知,函数分几种情况,但它们都有一个共同的特点,即:这些函数的执行结果就是最终的数据。
我们可以发现 mergedDataFn
和 mergedInstanceDataFn
这两个函数有一个共同的特点,内部都调用了 mergeData
处理数据并返回,我们先看一下 mergedDataFn
函数,其源码如下:
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
这个函数直接返回了 mergeData
函数的执行结果,再看看 mergedInstanceDataFn
函数,其源码如下:
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
我们注意到 mergedDataFn
和 mergedInstanceDataFn
这两个函数都有类似这样的代码:
typeof childVal === 'function' ? childVal.call(this, this) : childVal
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
我们知道 childVal
要么是子组件的选项,要么是使用 new
操作符创建实例时的选项,无论是哪一种,总之 childVal
要么是函数,要么就是一个纯对象。所以如果是函数的话就通过执行该函数从而获取到一个纯对象,所以类似上面那段代码中判断 childVal
和 parentVal
的类型是否是函数的目的只有一个,获取数据对象(纯对象)。所以 mergedDataFn
和 mergedInstanceDataFn
函数内部调用 mergeData
方法时传递的两个参数就是两个纯对象(当然你可以简单的理解为两个JSON对象)。
所以说既然知道了 mergeData
函数接收的两个参数就是两个纯对象,那么再看 mergeData
函数的代码就轻松多了,它才是终极合并策略,其源码如下:
/**
* Helper that recursively merges two data objects together.
*/
function mergeData (to: Object, from: ?Object): Object {
// 没有 from 直接返回 to
if (!from) return to
let key, toVal, fromVal
const keys = Object.keys(from)
// 遍历 from 的 key
for (let i = 0; i < keys.length; i++) {
key = keys[i]
toVal = to[key]
fromVal = from[key]
// 如果 from 对象中的 key 不在 to 对象中,则使用 set 函数为 to 对象设置 key 及相应的值
if (!hasOwn(to, key)) {
set(to, key, fromVal)
// 如果 from 对象中的 key 也在 to 对象中,且这两个属性的值都是纯对象则递归进行深度合并
} else if (isPlainObject(toVal) && isPlainObject(fromVal)) {
mergeData(toVal, fromVal)
}
// 其他情况什么都不做
}
return to
}
mergeData
函数接收两个参数 to
和 from
,根据 mergeData
函数被调用时参数的传递顺序我们知道,to
对应的是 childVal
产生的纯对象,from
对应 parentVal
产生的纯对象,我们看 mergeData
第一句代码:
if (!from) return to
如果没有 from
则直接返回 to
,也就是说如果没有 parentVal
产生的值,就直接使用 childVal
产生的值。
如果有 parentVal
产生的值,则代码继续向下运行,我们看 mergeData
最后的返回值:
return to
其返回的仍是 to
对象,所以你应该能猜的到 mergeData
函数的作用,可以简单理解为:将 from
对象的属性混合到 to
对象中,也可以说是将 parentVal
对象的属性混合到 childVal
中,最后返回的是处理后的 childVal
对象。
mergeData
的具体做法就是像上面 mergeData
函数的代码段中所注释的那样,对 from
对象的 key
进行遍历:
如果
from
对象中的key
不在to
对象中,则使用set
函数为to
对象设置key
及相应的值。如果
from
对象中的key
在to
对象中,且这两个属性的值都是纯对象则递归地调用mergeData
函数进行深度合并。其他情况不做处理。
上面提到了一个 set
函数,根据 options.js
文件头部的引用关系可知:这个函数来自于 core/observer/index.js
文件,实际上这个 set
函数就是 Vue
暴露给我们的全局API Vue.set
。在这里由于我们还没有讲到 set
函数的具体实现,所以你就可以简单理解为 set
函数的功能与我们前面遇到过的 extend
工具函数功能相似即可。
所以我们知道了 mergeData
函数的执行结果才是真正的数据对象,由于 mergedDataFn
和 mergedInstanceDataFn
这两个函数的返回值就是 mergeData
函数的执行结果,所以 mergedDataFn
和 mergedInstanceDataFn
函数的执行将会得到数据对象,我们还知道 data
选项会被 mergeOptions
处理成函数,比如处理成 mergedInstanceDataFn
,所以:最终得到的 data
选项是一个函数,且该函数的执行结果就是最终的数据对象。
最后我们对大家经常会产生疑问的地方做一些补充:
strats.data
会被处理成一个函数?
一、为什么最终 这是因为,通过函数返回数据对象,保证了每个组件实例都有一个唯一的数据副本,避免了组件间数据互相影响。后面讲到 Vue
的初始化的时候大家会看到,在初始化数据状态的时候,就是通过执行 strats.data
函数来获取数据并对其进行处理的。
二、为什么不在合并阶段就把数据合并好,而是要等到初始化的时候再合并数据?
这个问题是什么意思呢?我们知道在合并阶段 strats.data
将被处理成一个函数,但是这个函数并没有被执行,而是到了后面初始化的阶段才执行的,这个时候才会调用 mergeData
对数据进行合并处理,那这么做的目的是什么呢?
其实这么做是有原因的,后面讲到 Vue
的初始化的时候,大家就会发现 inject
和 props
这两个选项的初始化是先于 data
选项的,这就保证了我们能够使用 props
初始化 data
中的数据,如下:
// 子组件:使用 props 初始化子组件的 childData
const Child = {
template: '<span></span>',
data () {
return {
childData: this.parentData
}
},
props: ['parentData'],
created () {
// 这里将输出 parent
console.log(this.childData)
}
}
var vm = new Vue({
el: '#app',
// 通过 props 向子组件传递数据
template: '<child parent-data="parent" />',
components: {
Child
}
})
如上例所示,子组件的数据 childData
的初始值就是 parentData
这个 props
。而之所以能够这样做的原因有两个
- 1、由于
props
的初始化先于data
选项的初始化 - 2、
data
选项是在初始化的时候才求值的,你也可以理解为在初始化的时候才使用mergeData
进行数据合并。
三、你可以这么做。
在上面的例子中,子组件的 data
选项我们是这么写的:
data () {
return {
childData: this.parentData
}
}
但你知道吗,你也可以这么写:
data (vm) {
return {
childData: vm.parentData
}
}
// 或者使用更简单的解构赋值
data ({ parentData }) {
return {
childData: parentData
}
}
我们可以通过解构赋值的方式,也就是说 data
函数的参数就是当前实例对象。那么这个参数是在哪里传递进来的呢?其实有两个地方,其中一个地方我们前面见过了,如下面这段代码:
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
注意这里的 childVal.call(this, this)
和 parentVal.call(this, this)
,关键在于 call(this, this)
,可以看到,第一个 this
指定了 data
函数的作用域,而第二个 this
就是传递给 data
函数的参数。
当然了仅仅在这里这么做是不够的,比如 mergedDataFn
前面的代码:
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
在这段代码中,直接将 parentVal
或 childVal
返回了,我们知道这里的 parentVal
和 childVal
就是 data
函数,由于被直接返回,所以并没有指定其运行的作用域,且也没有传递当前实例作为参数,所以我们必然还是在其他地方做这些事情,而这个地方就是我们说的第二个地方,它在哪里呢?当然是初始化的时候,后面我们会讲到的,如果这里大家没有理解也不用担心。
生命周期钩子选项的合并策略
现在我们看完了 strats.data
策略函数,我们继续按照 options.js
文件的顺序看代码,接下来的一段代码如下:
/**
* Hooks and props are merged as arrays.
*/
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
看上去,这段代码貌似是用来合并生命周期钩子的,事实上的确是这样,我们看看它是怎么做的,首先上面的代码由两部分组成:mergeHook
函数和一个 forEach
语句。我们先看下面的 forEach
语句:
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
使用 forEach
遍历 LIFECYCLE_HOOKS
常量,那说明这个常量应该是一个数组,我们根据 options.js
文件头部的引用关系可知 LIFECYCLE_HOOKS
常量来自于 shared/constants.js
文件,我们打开这个文件找到 LIFECYCLE_HOOKS
常量如下:
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
可以发现 LIFECYCLE_HOOKS
常量实际上是由与生命周期钩子同名的字符串组成的数组。
所以现在再回头来看那段 forEach
语句可知,它的作用就是在 strats
策略对象上添加用来合并各个生命周期钩子选项的策略函数,并且这些生命周期钩子选项的策略函数相同:都是 mergeHook
函数。
那么 mergeHook
函数是怎样合并生命周期选项的呢?我们看看 mergeHook
函数的代码,如下:
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
整个函数体由三组三目运算符组成,有一点值得大家学习的就是这里写三目运算符的方式,是不是感觉非常地清晰易读?那么这段代码的分析我们同样使用与上面代码相同的格式来写:
return (是否有 childVal,即判断组件的选项中是否有对应名字的生命周期钩子函数)
? 如果有 childVal 则判断是否有 parentVal
? 如果有 parentVal 则使用 concat 方法将二者合并为一个数组
: 如果没有 parentVal 则判断 childVal 是不是一个数组
? 如果 childVal 是一个数组则直接返回
: 否则将其作为数组的元素,然后返回数组
: 如果没有 childVal 则直接返回 parentVal
如上就是对 mergeHook
函数的解读,我们可以发现,在经过 mergeHook
函数处理之后,组件选项的生命周期钩子函数被合并成一个数组。第一个三目运算符需要注意,它判断是否有 childVal
,即组件的选项是否写了生命周期钩子函数,如果没有则直接返回了 parentVal
,这里有个问题:parentVal
一定是数组吗?答案是:如果有 parentVal
那么其一定是数组,如果没有 parentVal
那么 strats[hooks]
函数根本不会执行。我们以 created
生命周期钩子函数为例:
如下代码:
new Vue({
created: function () {
console.log('created')
}
})
如果以这段代码为例,那么对于 strats.created
策略函数来讲(注意这里的 strats.created
就是 mergeHooks
),childVal
就是我们例子中的 created
选项,它是一个函数。parentVal
应该是 Vue.options.created
,但 Vue.options.created
是不存在的,所以最终经过 strats.created
函数的处理将返回一个数组:
options.created = [
function () {
console.log('created')
}
]
再看下面的例子:
const Parent = Vue.extend({
created: function () {
console.log('parentVal')
}
})
const Child = new Parent({
created: function () {
console.log('childVal')
}
})
其中 Child
是使用 new Parent
生成的,所以对于 Child
来讲,childVal
是:
created: function () {
console.log('childVal')
}
而 parentVal
已经不是 Vue.options.created
了,而是 Parent.options.created
,那么 Parent.options.created
是什么呢?它其实是通过 Vue.extend
函数内部的 mergeOptions
处理过的,所以它应该是这样的:
Parent.options.created = [
created: function () {
console.log('parentVal')
}
]
所以这个例子最终的结果就是既有 childVal
,又有 parentVal
,那么根据 mergeHooks
函数的逻辑:
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
// 这里,合并且生成一个新数组
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
关键在这句:parentVal.concat(childVal)
,将 parentVal
和 childVal
合并成一个数组。所以最终结果如下:
[
created: function () {
console.log('parentVal')
},
created: function () {
console.log('childVal')
}
]
另外我们注意第三个三目运算符:
: Array.isArray(childVal)
? childVal
: [childVal]
它判断了 childVal
是不是数组,这说明什么?说明了生命周期钩子是可以写成数组的,虽然 Vue
的文档里没有,不信你可以试试:
new Vue({
created: [
function () {
console.log('first')
},
function () {
console.log('second')
},
function () {
console.log('third')
}
]
})
钩子函数将按顺序执行。
资源(assets)选项的合并策略
在 Vue
中 directives
、filters
以及 components
被认为是资源,其实很好理解,指令、过滤器和组件都是可以作为第三方应用来提供的,比如你需要一个模拟滚动的组件,你当然可以选用超级强大的第三方组件 scroll-flip-page,所以这样看来 scroll-flip-page 就可以认为是资源,除了组件之外指令和过滤器也都是同样的道理。
而我们接下来要看的代码就是用来合并处理 directives
、filters
以及 components
等资源选项的,看如下代码:
/**
* Assets
*
* When a vm is present (instance creation), we need to do
* a three-way merge between constructor options, instance
* options and parent options.
*/
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
与生命周期钩子的合并处理策略基本一致,以上代码段也分为两部分:mergeAssets
函数以及一个 forEach
语句。我们同样先看 forEach
语句,这个 forEach
循环用来遍历 ASSET_TYPES
常量,根据 options.js
文件头部的引用关系可知 ASSET_TYPES
常量来自于 shared/constants.js
文件,我们打开 shared/constants.js
文件找到 ASSET_TYPES
常量如下:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
我们发现 ASSET_TYPES
其实是由与资源选项“同名”的三个字符串组成的数组,注意所谓的“同名”是带引号的,因为数组中的字符串与真正的资源选项名字相比要少一个字符 s
。
ASSET_TYPES | 资源选项名字 |
---|---|
component | components |
directive | directives |
filter | filters |
所以我们再看一下那段 forEach
语句:
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
我们发现在循环内部它有手动拼接上一个 's'
,所以最终的结果就是在 strats
策略对象上添加与资源选项名字相同的策略函数,用来分别合并处理三类资源。所以接下来我们就看看它是怎么处理的,mergeAssets
代码如下:
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
上面的代码本身逻辑很简单,首先以 parentVal
为原型创建对象 res
,然后判断是否有 childVal
,如果有的话使用 extend
函数将 childVal
上的属性混合到 res
对象上并返回。如果没有 childVal
则直接返回 res
。
举个例子,大家知道任何组件的模板中我们都可以直接使用 <transition/>
组件或者 <keep-alive/>
等,但是我们并没有在我们自己的组件实例的 components
选项中显式地声明这些组件。那么这是怎么做到的呢?其实答案就在 mergeAssets
函数中。以下面的代码为例:
var v = new Vue({
el: '#app',
components: {
ChildComponent: ChildComponent
}
})
上面的代码中,我们创建了一个 Vue
实例,并注册了一个子组件 ChildComponent
,此时 mergeAssets
方法内的 childVal
就是例子中的 components
选项:
components: {
ChildComponent: ChildComponent
}
而 parentVal
就是 Vue.options.components
,我们知道 Vue.options
如下:
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: Object.create(null),
directives:{
model,
show
},
filters: Object.create(null),
_base: Vue
}
所以 Vue.options.components
就应该是一个对象:
{
KeepAlive,
Transition,
TransitionGroup
}
也就是说 parentVal
就是如上包含三个内置组件的对象,所以经过如下这句话之后:
const res = Object.create(parentVal || null)
你可以通过 res.KeepAlive
访问到 KeepAlive
对象,因为虽然 res
对象自身属性没有 KeepAlive
,但是它的原型上有。
然后再经过 return extend(res, childVal)
这句话之后,res
变量将被添加 ChildComponent
属性,最终 res
如下:
res = {
ChildComponent
// 原型
__proto__: {
KeepAlive,
Transition,
TransitionGroup
}
}
所以这就是为什么我们不用显式地注册组件就能够使用一些内置组件的原因,同时这也是内置组件的实现方式,通过 Vue.extend
创建出来的子类也是一样的道理,一层一层地通过原型进行组件的搜索。
最后说一下 mergeAssets
函数中的这句话:
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
在非生产环境下,会调用 assertObjectType
函数,这个函数其实是用来检测 childVal
是不是一个纯对象的,如果不是纯对象会给你一个警告,其源码很简单,如下:
function assertObjectType (name: string, value: any, vm: ?Component) {
if (!isPlainObject(value)) {
warn(
`Invalid value for option "${name}": expected an Object, ` +
`but got ${toRawType(value)}.`,
vm
)
}
}
就是使用 isPlainObject
进行判断。上面我们都在以 components
进行讲解,对于指令(directives
)和过滤器(filters
)也是一样的,因为他们都是用 mergeAssets
进行合并处理。
选项 watch 的合并策略
接下来我们要看的代码就是这一段了:
/**
* Watchers.
*
* Watchers hashes should not overwrite one
* another, so we merge them as arrays.
*/
strats.watch = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = {}
extend(ret, parentVal)
for (const key in childVal) {
let parent = ret[key]
const child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
}
这一段代码的作用是在 strats
策略对象上添加 watch
策略函数。所以 strats.watch
策略函数应该是合并处理 watch
选项的。我们先看函数体开头的两句代码:
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
其中 nativeWatch
来自于 core/util/env.js
文件,大家可以在 core/util 目录下的工具方法全解 中查看其作用。在 Firefox
浏览器中 Object.prototype
拥有原生的 watch
函数,所以即便一个普通的对象你没有定义 watch
属性,但是依然可以通过原型链访问到原生的 watch
属性,这就会给 Vue
在处理选项的时候造成迷惑,因为 Vue
也提供了一个叫做 watch
的选项,即使你的组件选项中没有写 watch
选项,但是 Vue
通过原型访问到了原生的 watch
。这不是我们想要的,所以上面两句代码的目的是一个变通方案,当发现组件选项是浏览器原生的 watch
时,那说明用户并没有提供 Vue
的 watch
选项,直接重置为 undefined
。
然后是这句代码:
if (!childVal) return Object.create(parentVal || null)
检测了是否有 childVal
,即组件选项是否有 watch
选项,如果没有的话,直接以 parentVal
为原型创建对象并返回(如果有 parentVal
的话)。
如果组件选项中有 watch
选项,即 childVal
存在,则代码继续执行,接下来将执行这段代码:
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
由于此时 childVal
存在,所以在非生产环境下使用 assertObjectType
函数对 childVal
进行类型检测,检测其是否是一个纯对象,我们知道 Vue
的 watch
选项需要是一个纯对象。接着判断是否有 parentVal
,如果没有的话则直接返回 childVal
,即直接使用组件选项的 watch
。
如果存在 parentVal
,那么代码继续执行,此时 parentVal
以及 childVal
都将存在,那么就需要做合并处理了,也就是下面要执行的代码:
// 定义 ret 常量,其值为一个对象
const ret = {}
// 将 parentVal 的属性混合到 ret 中,后面处理的都将是 ret 对象,最后返回的也是 ret 对象
extend(ret, parentVal)
// 遍历 childVal
for (const key in childVal) {
// 由于遍历的是 childVal,所以 key 是子选项的 key,父选项中未必能获取到值,所以 parent 未必有值
let parent = ret[key]
// child 是肯定有值的,因为遍历的就是 childVal 本身
const child = childVal[key]
// 这个 if 分支的作用就是如果 parent 存在,就将其转为数组
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
ret[key] = parent
// 最后,如果 parent 存在,此时的 parent 应该已经被转为数组了,所以直接将 child concat 进去
? parent.concat(child)
// 如果 parent 不存在,直接将 child 转为数组返回
: Array.isArray(child) ? child : [child]
}
// 最后返回新的 ret 对象
return ret
上面的代码段中写了很详细的注释。首先定义了 ret
常量,最后返回的也是 ret
常量,所以中间的代码是在充实 ret
常量。之后使用 extend
函数将 parentVal
的属性混合到 ret
中。然后开始一个 for in
循环遍历 childVal
,这个循环的目的是:检测子选项中的值是否也在父选项中,如果在的话将父子选项合并到一个数组,否则直接把子选项变成一个数组返回。
举个例子:
// 创建子类
const Sub = Vue.extend({
// 检测 test 的变化
watch: {
test: function () {
console.log('extend: test change')
}
}
})
// 使用子类创建实例
const v = new Sub({
el: '#app',
data: {
test: 1
},
// 检测 test 的变化
watch: {
test: function () {
console.log('instance: test change')
}
}
})
// 修改 test 的值
v.test = 2
上面的代码中,当我们修改 v.test
的值时,两个观察 test
变化的函数都将被执行。
我们使用子类 Sub
创建了实例 v
,对于实例 v
来讲,其 childVal
就是组件选项的 watch
:
watch: {
test: function () {
console.log('instance: test change')
}
}
而其 parentVal
就是 Sub.options
,实际上就是:
watch: {
test: function () {
console.log('extend: test change')
}
}
最终这两个 watch
选项将被合并为一个数组:
watch: {
test: [
function () {
console.log('extend: test change')
},
function () {
console.log('instance: test change')
}
]
}
我们可以通过打印实例的 $options
属性来确认这一点:
console.log(v.$options)
如下图:
可以发现 watch.test
变成了数组,但是 watch.test
并不一定总是数组,只有父选项(parentVal
)也存在对该字段的观测时它才是数组,如下:
// 创建实例
const v = new Vue({
el: '#app',
data: {
test: 1
},
// 检测 test 的变化
watch: {
test: function () {
console.log('instance: test change')
}
}
})
// 修改 test 的值
v.test = 2
我们直接使用 Vue
创建实例,这个时候对于实例 v
来说,父选项是 Vue.options
,由于 Vue.options
并没有 watch
选项,所以逻辑将直接在 strats.watch
函数的这句话中返回:
if (!parentVal) return childVal
没有 parentVal
即父选项中没有 watch
选项,则直接返回 childVal
,也就是直接返回了子选项的 watch
选项,如就是例子中写的对象:
{
test: function () {
console.log('instance: test change')
}
}
所以此时 test
字段就不再是数组了,而就是一个函数,同样可以通过打印实例的 $options
选项证明:
所以大家应该知道:被合并处理后的 watch
选项下的每个键值,有可能是一个数组,也有可能是一个函数。
选项 props、methods、inject、computed 的合并策略
接下来我们要看的一段代码如下:
/**
* Other object hashes.
*/
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
if (childVal && process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
}
这段代码的作用是在 strats
策略对象上添加 props
、methods
、inject
以及 computed
策略函数,顾名思义这些策略函数是分别用来合并处理同名选项的,并且所使用的策略相同。
对于 props
、methods
、inject
以及 computed
这四个选项有一个共同点,就是它们的结构都是纯对象,虽然我们在书写 props
或者 inject
选项的时候可能是一个数组,但是在 Vue的思路之选项的规范化 一节中我们知道,Vue
内部都将其规范化为了一个对象。所以我们看看 Vue
是如何处理这些对象散列的。
策略函数内容如下:
// 如果存在 childVal,那么在非生产环境下要检查 childVal 的类型
if (childVal && process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
// parentVal 不存在的情况下直接返回 childVal
if (!parentVal) return childVal
// 如果 parentVal 存在,则创建 ret 对象,然后分别将 parentVal 和 childVal 的属性混合到 ret 中,注意:由于 childVal 将覆盖 parentVal 的同名属性
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
// 最后返回 ret 对象。
return ret
首先,会检测 childVal
是否存在,即子选项是否有相关的属性,如果有的话在非生产环境下需要使用 assertObjectType
检测其类型,保证其类型是纯对象。然后会判断 parentVal
是否存在,不存在的话直接返回子选项。
如果 parentVal
存在,则使用 extend
方法将其属性混合到新对象 ret
中,如果 childVal
也存在的话,那么同样会再使用 extend
函数将其属性混合到 ret
中,所以如果父子选项中有相同的键,那么子选项会把父选项覆盖掉。
以上就是 props
、methods
、inject
以及 computed
这四个属性的通用合并策略。
选项 provide 的合并策略
最后一个选项的合并策略,就是 provide
选项的合并策略,只有一句代码,如下:
strats.provide = mergeDataOrFn
也就是说 provide
选项的合并策略与 data
选项的合并策略相同,都是使用 mergeDataOrFn
函数。
选项处理小结
现在我们了解了 Vue
中是如何合并处理选项的,接下来我们稍微做一个总结:
- 对于
el
、propsData
选项使用默认的合并策略defaultStrat
。 - 对于
data
选项,使用mergeDataOrFn
函数进行处理,最终结果是data
选项将变成一个函数,且该函数的执行结果为真正的数据对象。 - 对于
生命周期钩子
选项,将合并成数组,使得父子选项中的钩子函数都能够被执行 - 对于
directives
、filters
以及components
等资源选项,父子选项将以原型链的形式被处理,正是因为这样我们才能够在任何地方都使用内置组件、指令等。 - 对于
watch
选项的合并处理,类似于生命周期钩子,如果父子选项都有相同的观测字段,将被合并为数组,这样观察者都将被执行。 - 对于
props
、methods
、inject
、computed
选项,父选项始终可用,但是子选项会覆盖同名的父选项字段。 - 对于
provide
选项,其合并策略使用与data
选项相同的mergeDataOrFn
函数。 - 最后,以上没有提及到的选项都将使默认选项
defaultStrat
。 - 最最后,默认合并策略函数
defaultStrat
的策略是:只要子选项不是undefined
就使用子选项,否则使用父选项。
至此,我们大概介绍完了 Vue
对选项的处理,但留心的同学一定注意到了,options.js
文件的代码我们都基本逐行分析,唯独剩下一个函数我们始终没有提到,它就是 resolveAsset
函数。这个函数我们暂且不在这里讲,后面随着我们的深入,自然会再次碰到它,到那个时候应该是讲它的最好时机。
再看 mixins 和 extends
在 Vue选项的规范化 一节中,我们讲到了 mergeOptions
函数中的如下这段代码:
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)
}
}
当时候我们并没有深入讲解,因为当时我们还不了解 mergeOptions
函数的作用,但是现在我们可以回头来看一下这段代码了。
我们知道 mixins
在 Vue
中用于解决代码复用的问题,比如混入 created
生命周期钩子,用于打印一句话:
const consoleMixin = {
created () {
console.log('created:mixins')
}
}
new Vue ({
mixins: [consoleMixin],
created () {
console.log('created:instance')
}
})
运行以上代码,将打印两句话:
// created:mixins
// created:instance
这是因为 mergeOptions
函数在处理 mixins
选项的时候递归调用了 mergeOptions
函数将 mixins
合并到了 parent
中,并将合并后生成的新对象作为新的 parent
:
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
上例中我们只涉及到 created
生命周期钩子的合并,所以会使用生命周期钩子的合并策略函数进行处理,现在我们已经知道 mergeOptions
会把生命周期选项合并为一个数组,所以所有的生命周期钩子都会被执行。那么不仅仅是生命周期钩子,任何写在 mixins
中的选项,都会使用 mergeOptions
中相应的合并策略进行处理,这就是 mixins
的实现方式。
对于 extends
选项,与 mixins
相同,甚至由于 extends
选项只能是一个对象,而不能是数组,反而要比 mixins
的实现更为简单,连遍历都不需要。