Vue2.0 源码分析笔记(四)选项的合并
一、为什么最终 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 函数,由于被直接返回,所以并没有指定其运行的作用域,且也没有传递当前实例作为参数,所以我们必然还是在其他地方做这些事情,而这个地方就是我们说的第二个地方,它在哪里呢?当然是初始化的时候,后面我们会讲到的,如果这里大家没有理解也不用担心。
四、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
根据 mergeHooks返回值,其返回值是一个数组,因此我们在写生命周期时,可以直接传递数组,并且其将按照顺序执行。
new Vue({
created: [
function () {
console.log('first')
},
function () {
console.log('second')
},
function () {
console.log('third')
}
]
})
五、资源(assets)选项的合并策略
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
}
}
举个例子,大家知道任何组件的模板中我们都可以直接使用 <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 创建出来的子类也是一样的道理,一层一层地通过原型进行组件的搜索。

六、选项处理小结
现在我们了解了 Vue 中是如何合并处理选项的,接下来我们稍微做一个总结:
- 对于 el、propsData 选项使用默认的合并策略 defaultStrat。
- 对于 data 选项,使用 mergeDataOrFn 函数进行处理,最终结果是 data 选项将变成一个函数,且该函数的执行结果为真正的数据对象。
- 对于 生命周期钩子 选项,将合并成数组,使得父子选项中的钩子函数都能够被执行
- 对于 directives、filters 以及 components 等资源选项,父子选项将以原型链的形式被处理,正是因为这样我们才能够在任何地方都使用内置组件、指令等。
- 对于 watch 选项的合并处理,类似于生命周期钩子,如果父子选项都有相同的观测字段,将被合并为数组,这样观察者都将被执行。
- 对于 props、methods、inject、computed 选项,父选项始终可用,但是子选项会覆盖同名的父选项字段。
- 对于 provide 选项,其合并策略使用与 data 选项相同的 mergeDataOrFn 函数。
最后,以上没有提及到的选项都将使默认选项 defaultStrat。
最最后,默认合并策略函数 defaultStrat 的策略是:只要子选项不是 undefined 就使用子选项,否则使用父选项。