参数合并

10/28/2021 vue

我们知道在new Vue时会传入一个参数对象,比如:

new Vue({
	el: '#app',
	data: {
		name: 'zhangsan',
		age: 20
	},
	components: {
		HelloWorld
	},
	computed: {
		getInfo () {
			return `姓名: ${this.name}, 年龄: ${this.age}`
		}
	},
	methods: {
		getName () {
			return this.name
		}
	}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在实例化Vue实例时,会在源码中进行一次参数合并,在src/core/instance/init.js中的Vue.prototype._init方法中可以看到有mergeOptions这样一个方法,顾名思义就是配置合并

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
		
    // ......
		
    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),
        options || {},
        vm
      )
    }
		
    // ......

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

提示

需要注意的是,Vue实例的参数合并在Vue.prototype._init中,而组件实例的参数合并在Vue.extend中,后面组件部分会介绍

# resolveConstructorOptions

在合并参数之前,调用了resolveConstructorOptions方法,传递的参数为实例的constructor属性,我们知道实例的constructor属性会指向构造函数,所以实际上传递的就是Vue构造函数

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    // 先省略这部分代码,主要是用在实例化 Vue.extend 方法返回的构造函数,后续再介绍
    // ......
  }
  return options
}
1
2
3
4
5
6
7
8

所以实际上resolveConstructorOptions方法返回的就是Vue.options, 在src/core/global-api/index.js中定义了很多Vue的属性,比如Vue.setVue.deleteVue.nextTick...,不过我们先分析Vue.options

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)
1
2
3
4
5
6
7
8
9
10

使用Object.create(null)方法创建一个没有原型链的对象,赋值给Vue.options,接着循环遍历常量ASSET_TYPES数组中的每一项,作为Vue.options中的属性,在src/shared/constants.js中可查看对ASSET_TYPES的定义

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
1
2
3
4
5

Vue.options._base设置为对Vue的引用,并利用extend方法,将内置组件KeepAlive添加到Vue.options.components中,可在src/shared/util.js中查看extend工具函数的定义,在src/core/components/index.js中查看导出的内置组件对象KeepAlive

export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

// src/core/components/index.js
import KeepAlive from './keep-alive'

export default {
  KeepAlive
}
1
2
3
4
5
6
7
8
9
10
11
12
13

src/platforms/web/runtime/index.js中也对Vue.options.directivesVue.options.components进行了扩展

extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
1
2

platformDirectives定义在src/platforms/web/runtime/directives/index.js中,主要是内置的指令v-modelv-showplatformComponents定义在src/platforms/web/runtime/components/index.js中,包含内置组件transitiontransition-group

// src/platforms/web/runtime/components/index.js
import Transition from './transition'
import TransitionGroup from './transition-group'

export default {
  Transition,
  TransitionGroup
}

// src/platforms/web/runtime/directives/index.js
import model from './model'
import show from './show'

export default {
  model,
  show
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# mergeOptions

src/core/util/options.js中,可以看到mergeOptions方法的定义,在Vue.prototype._init中的mergeOptions方法调用中传递了三个参数Vue.optionsoptions对象,和vm实例,分别对应mergeOptionsparentchildvm三个形参,且vm参数可选

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child) // 验证options.components中的组件名称
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm) // 规范化props
  normalizeInject(child, vm) // 规范化Inject
  normalizeDirectives(child) // 规范化directives
	
  // ......

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

接着在mergeOptions中调用normalizeProps规范化propsnormalizeProps方法和mergeOptions方法在同一文件中

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val) // 将prop名称转为驼峰形式并返回
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } 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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

normalizeProps方法逻辑很简单,定义res对象作为规范后的props,遍历options.props中的每一项prop,并对每个prop名称使用camelize方法转换为驼峰形式,(例如:将data-info转换为dataInfo),然后添加到res对象中,然后将res对象重新赋值给options.props

接着调用normalizeInject方法和normalizeDirectives方法,逻辑都很简单明了,normalizeInject方法同normalizeProps方法思想一样,也是定义一个对象然后循环遍历重新赋值,这里就只分析下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 }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

normalizeDirectives方法将options.directives中的所有自定义指令循环遍历一次,做一次函数类型判断,如果是函数类型,则重新赋值为一个对象,并且默认包含bindupdate,关于bindupdate的使用可以查看官方文档自定义指令 (opens new window)部分

继续回到mergeOptions方法中

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component

  // ......

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  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)
      }
    }
  }

  // ......

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

接着做一层child._base判断,确保child为原始选项对象,而不是合并后的对象,因为只有parent参数对象和child参数对象合并过后才会有_base属性(上面说明过parent形参对应Vue.options,而Vue.options中存在_base属性,所以只有合并后才会存在_base属性)

如果参数对象child中含有extendsmixins属性,则会递归调用mergeOptions方法

继续看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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

循环遍历parentchild中的每个属性并且调用mergeField方法,在遍历child中时多了一层判断,只有当遍历的属性不存在与parent中时才会调用mergeField方法,避免重复调用,在mergeField中,会根据不同的参数key获取不同的合并策略,然后进行合并,如果未定义相关key的合并策略,则默认使用defaultStrat进行合并,parent._base使用的就是defaultStrat方法进行的合并

先简单看下defaultStrat合并方法

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}
1
2
3
4
5

defaultStrat只是做了一层简单的判断

# strats(合并策略对象)

strats合并策略对象和mergeOptions方法都在一个文件中定义,strats指向config.optionMergeStrategiesconfig对象为src/core/config.js中的默认导出对象

// src/core/config.js
export default ({

  optionMergeStrategies: Object.create(null),
	
  // ......

}: Config)


// src/core/util/options.js
import config from '../config'

// ......

const strats = config.optionMergeStrategies

// ......
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

strats中定义了很多合并策略,可以在src/core/util/options.js中详细查看,这里不一一说明,可以自行查看,逻辑都挺简单的,这里主要分析下datapropswatch的合并

# data

data的合并时,先对vm做了层判断,看是否存在

注意

vm为根实例时,mergeOptions的第三个参数vm是存在的,当为Vue.extend返回的构造函数的实例时,进行参数合并,vm参数没有传递

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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

vm不存在,则说明vmVue.extend返回的构造函数的实例,如果childVal类型不为function,则会报错,这就在源码层面解释了为什么组件中的data选项必须为一个function,接着都调用了mergeDataOrFn方法

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // Vue.extend返回的构造函数参数合并
    // 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 {
    // vue根实例的参数和并
    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
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

mergeDataOrFn中同样是对vm做了层是否存在的判断,并且都返回一个合并函数,当为根实例的参数合并时,返回mergedInstanceDataFn方法,且在内部对childVal进行了function类型判断,说明new Vue的参数对象中的data既可以为返回一个对象的function,也可以直接为一个对象,当为Vue.extend返回的构造函数的实例的参数合并时,返回mergedDataFn方法,且他们都在返回的函数中调用了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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

mergeData方法中对from参数对象中的每一项进行遍历并获取对应key的值,如果对应的key不存在于to对象中,则为to对象设置对应的key和值,如果存在,且在tofrom中都为一个对象,则递归调用mergeData方法进行合并

综上就是对data参数的合并策略分析,单看代码,可能有点晦涩难懂,可以使用下列例子进行分析 Vue根实例的参数合并

const mixin = {
	data: {
		name: '张三',
		info: {
			age: 20,
			addr: '四川省巴中市'
		}
	}
}

new Vue({
	mixins: [mixin],
	data: {
		name: '李四',
		info: {
			sex: '男'
		}
	}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

根据上述的源码分析,Vue.options会先和new Vue参数项的mixins数组中的每一项mixin进行合并,并将返回的对象和new Vue的中其余的参数项进行合并 Vue.extend返回的构造函数的实例的参数合并

const mixin = {
	data () {
		return {
			name: '张三',
			info: {
				age: 20,
				addr: '四川省巴中市'
			}
		}
	}
}
new Vue.extend({
	mixins: [mixin],
	data () {
		return {
			name: '李四',
			info: {
				sex: '男'
			}
		}
	}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

需要注意的是Vue.extend参数对象中的data必须为function类型,同样的,Vue.options也会先和mixins中的每一项进行合并,再和其余项进行合并

# props

propsmethodscomputed都使用的相同的合并策略

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

使用Object.create(null)创建原型链为null的对象,利用对象中的相同的key后面的key的值会替换掉前面的key的值的原理进行合并

# watch

watch相对于props的合并会复杂一点,但也仅仅是相对!

strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {

  // ......

  /* istanbul ignore if */
  if (!childVal) return Object.create(parentVal || null)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal // 如果parentVal不存在,则直接返回childVal
  const ret = {}
  extend(ret, parentVal) // 将parentVal中的每一项都添加到ret中
  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 // 返回的ret对象中,每个key都对应一个
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

首先对childVal做是否存在判断,如果不存在则返回Object.create(parent || null)创建的对象,再利用assertObjectTypechildVal做对象类型的断言判断,接着判断parentVal如果不存在,则直接返回childVal,如果条件都不成立则创建一个ret对象,将parentVal对象浅拷贝到ret中,再循环遍历childVal中的每个key,并根据遍历的key获取ret对象和childVal对象中的监听器,然后将每一项都重新设为数组,如果retchildVal中都存在相同的监听器,则利用concat进行合并

最后更新时间: 12/4/2022, 1:44:46 PM