这是我参与更文挑战的第18天,活动详情查看: 更文挑战
前言
在前边学习创建Vue实例的时候,初始化vm对象的时候会合并参数,合并参数有很多合并策略,今天就来看下Vue具体是怎么进行参数合并的。
回顾下mergeOptions中最后是如何处理options的,定义在src/core/util/options中:
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
...
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
}
可以看到mergeField函数针对不同的key返回了不同的策略,它里边有两个变量:strats与defalutStrat,它们保存了具体的合并策略。
defaultStrat
默认的合并策略是先查属性在child上是否存在,不存在则使用parent上的值。
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
strats
先整体来看下strats都有什么:
看的出来,主要是生命周期钩子函数,我们Vue组件中定义的data,computed,filters,methods,props,watch等
看一下strats的初始定义,就是一个空的Object
const strats = config.optionMergeStrategies
optionMergeStrategies: Object.create(null)
生命周期函数合并
先来看下生命周期函数怎么合并的:
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
这里有个多层次嵌套三元运算符,首先判断childVal是否存在,如果不存在,则返回parentVal;
如果childVal存在,则判断parentVal是否存在,如果存在,则将childVal合并到parentVal返回
parentVal不存在,则先判断childVal是不是数组,如果是直接返回childVal数组,否则将childVal包装成数组返回。
最后通过dedupeHooks函数将合并后的钩子整合成一个数组返回。
data合并
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)
}
data合并主要调用了mergeDataOrFn,接下来看下这个函数:
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
}
}
}
}
如果不存在vm实例(通过Vue.extend创建),则先判断是否存在childVal,如果不存在返回parentVal;接下来判断是否存在parentVal,如果不存在则直接返回childVal。如果两者都存在,则调用mergeData
如果存在vm(通过new Vue创建),在执行childVal函数和parentVal函数,this指向vm;最后直接调用mergeata合并
看下mergeData函数:
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
// in case the object is already observed...
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
set(to, key, fromVal)
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
return to
}
该函数接受两个参数to(childVal)和from(parentVal)
首先通过Reflect.ownKeys和Object.keys来判断是否有symbol属性;但是有一点要注意,Reflect.ownKeys返回对象上所有的自身属性,包括不可枚举的和Symbol,而Object.keys只枚举对象自身的可枚举属性,不包含Symbol。所以如果对象种有不可枚举属性,建议不要用这种判断方式。关于对象属性可以点这里。
遍历from对象中的所有属性,如果当前遍历的key已经是一个监听属性,则直接遍历下一个;否则从要合并的两个对象中拿出key对应的值,通过hasOwn函数(内部封装了hasOwnProperty)判断to对象自身是否存在同样的key,如果不存在,调用set函数绑定到to对象;
否则说明to对象本身就具有同样的key,则判断两个对象对应的值是否一致;如果不一致且两个对象都是Object,则递归执行mergeData。
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
watch合并
watch代码相对data来说简单很多。
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
}
首先是兼容性判断,火狐浏览器中,Object.prototype有个watch属性,所以要先排查watch对象不是浏览器自身的。(const nativeWatch = ({}).watch)
如果childVal为空,则调用Object.create创建以parentVal为原型对象的新对象,并返回该新对象
如果parentVal为空,则直接返回childVal
parentVal和childVal都不为空时,先调用extend(ret, parentVal)将parentVal浅复制给ret
接下来遍历childVal,如果当前项key在parent对象中也存在,将parent转化成数组,然后调用concat将child也一起拼接;否则返回数组格式的child给ret的key值。
看一下extend的实现:
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
props,methods,inject,computed合并
props,methods,inject,computed合并方式是一样的。内部主要是调用了extend方法。
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
}
- 首先创建一个空对象ret,将parentVal复制过去
- 接下来再将childVal复制到ret上,如果出现相同key值,直接覆盖。
provide合并
provide合并方式与data一致,都是调用mergeDataOrFn
strats.provide = mergeDataOrFn
components,directives,filters合并
components,directives,filters合并都是调用mergeAssets方法。
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
看一下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
}
}
mergeAssets实现也不复杂,先让res继承parentVal,然后判断childVal是否有值,如果有调用extend合并,否则直接返回。
Vue.mixin
Vue官方为我们提供了mixin的方式来分发组件中的复用功能,混入对象可以包含Vue组件的任意选项。
var mixin = {
...
}
new Vue({
mixins: [mixin],
...
})
文档也说明针对不同的选项,它具有不同的合并策略。
当组件与混入对象含有同名选项时,数据对象会递归合并,发生冲突时以组件数据优先。
同名钩子函数会合并为一个数组,混入对象的钩子函数在组件自身钩子函数之前。
methods,componets,directives合并为同一个对象,两个对象键名冲突时,取组件对象的键值对。
哈哈哈哈,看出来了把,这个完全符和我们上边的合并策略。重温一下这块的实现:
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
我们也可以直接调用Vue.mixin来实现一个全局混入,看一下官网的例子:
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: 'hello!'
})
来看一下Vue.mixin的内部实现:
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
Vue.mixin就是直接调用mergeOption,该mixin对象是我们源码中的childVal参数,是最高优先级的,所以使用全局钩子mixin一定要小心。
Vue3改动
前边我们知道data的合并是会递归去合并对象的,是深层次的合并。但是Vue3对这块做了处理,mixin和extend在进行data合并的时候,都变成了浅层次的合并,不会去递归查找Object。
官方给的例子:
const Mixin = {
data() {
return {
user: {
name: 'Jack',
id: 1
}
}
}
}
const CompA = {
mixins: [Mixin],
data() {
return {
user: {
id: 2
}
}
}
}
Vue2.x中$data结果:
{
user: {
id: 2,
name: 'Jack'
}
}
Vue3中$data结果:
{
user: {
id: 2
}
}