Vue的mergeOptions函数分析-上【Vue源码学习】

2019-03-25

Vue的mergeOptions函数的主要作用是用于合并选项(将俩个选项对象合并成一个),它是用于实例化和继承的核心函数。这也是为什么我们要去分析它。并且与函数相关的选项合并策略也都在一个文件里,定义在/src/core/util/options.js文件中。

使用场景

因为Vue的核心代码都是放在src文件夹下,所以我们可以在src目录下全局搜索下mergeOptions的使用场景,可以发现函数在Vue.extendVue.mixin实例化都有用到。(只考虑web平台)

// src/core/global-api/extend.js文件中
Vue.extend = function (extendOptions: Object): Function {
  // ... 忽略无关代码
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
}

// src/core/global-api/mixin.js文件中
Vue.mixin = function (mixin: Object) {
  this.options = mergeOptions(this.options, mixin)
  return this
}

// src/core/instance/init.js文件中 执行new 实例化的时候会执行
Vue.prototype._init = function (options?: Object) {
  // ... 忽略无关代码
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

这也证实了mergeOptions函数的注释所写的一样,Core utility used in both instantiation and inheritance.

逐行分析

mergeOptions函数被定义在/src/core/util/options.js文件中,源代码如下:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

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

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // 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)
      }
    }
  }

  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函数第三个参数是可选的,可以不传。Vue.mixinVue.extend函数中调用mergeOptions的时候是不传第三个参数的。选项的合并策略函数会根据vm参数来确定是实例化选项合并还是继承选项合并,从而做不同的处理,这个后面会详细讲到。

函数第一行,检查非生产环境下,执行checkComponents函数,该函数定义在同一文件下,主要是检查组件的名字是否符合规范。可以看到核心函数是validateComponentName,而且它被暴露出去,因为在Vue.component()Vue.extend()函数中都有用到。

if (process.env.NODE_ENV !== 'production') {
  checkComponents(child)
}

/**
 * 检验组件的名字
 */
function checkComponents (options: Object) {
  // 遍历对象的components属性,依次检验
  for (const key in options.components) {
    validateComponentName(key)
  }
}
// 如果检验不通过,给出相应警告
export function validateComponentName (name: string) {
  // 符合HTML5规范,由普通字符和中横线(-)组成,并且必须以字母开头。
  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.'
    )
  }
  // isBuiltInTag是检验名字不能与slot,component重名
  // isReservedTag是检验不能与html、svg内置标签重名
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}

接下来是检查传入的child是否是函数,如果是的话,取到它的options选项重新赋值给child。所以说child参数可以是普通选项对象,也可以是Vue构造函数和通过Vue.extend继承的子类构造函数。(Vue.options定义在src/core/global-api/index.js文件中)

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

再往后看有三个函数,分别是normalizePropsnormalizeInjectnormalizeDirectives,它们的作用是规范化选项,用过Vue的同学应该都知道,我们在写propsinject既可以是字符串数组,也可以是对象。directives既可以是一个函数,也可以是对象。Vue对外提供了便捷的写法,但内部处理要把他们规范成一样,才更方便处理。其实三个函数都是将选项转换对象的形式,接下来我们会逐个分析。

规范化props

function normalizeProps (options: Object, vm: ?Component) {
  // 定义props,是选项中的props属性的引用
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  // 1. 是数组的情况 例如:['name', 'age']
  if (Array.isArray(props)) {
    i = props.length
    // 循环遍历变成对象格式{ type: null }
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val) // 将key值变成驼峰形式
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        // 如果不是字符串数组,非生产环境给出警告
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    // 2. 是对象
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      // 如果是对象,则直接赋值,不是的话,则赋值type属性
      // 例如 { sex: String, job: { type: String, default: 'xxx' } }
      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
  options.props = res
}

normalizeProps函数还是比较简单的,如上图。当传入是字符串数组时(例如:['name', 'age']),说明只指定了key值,只需要将数组遍历,转成对象形式,把type属性设置null,当传入的是对象时,又分为俩种情况,一种是key值对应的是对象,那直接赋值就好。否则那代表只指定了类型(例如:{ sex: String, }),同样转成对象形式。

规范化inject

function normalizeInject (options: Object, vm: ?Component) {
  // 取到options.inject的引用
  const inject = options.inject
  if (!inject) return
  // 重置对象,之后重新赋值属性
  const normalized = options.inject = {}
  // 1. 数组情况,直接遍历。与normalizeProps同理
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) { 
    // 2. 对象情况。如果key值对应的是对象,则通过exntend合并,如果不是,则代表直接是from
    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
    )
  }
}

其实normalizeInject和normalizeProps函数很相似,都是分为对象和字符串数组俩种大情况,对象又分为俩种小情况。这里判断key值对应的是对象时,多做了一步处理就是用extend合并对象。因为from属性不能为空,所以如果对象中没有from属性,默认还是赋予同名的from。否则就会被覆盖。例如:如上图中的age属性中的from值parentAge就会覆盖默认的age,而job属性没有指定from,所以会赋予同名的from属性。

规范化directives

function normalizeDirectives (options: Object) {
  const dirs = options.directives
  // 遍历对象,如果key值对应的是函数。则修改成对象形式。
  // Vue提供了自定义指令的简写,如果只传函数,等同于{ bind: func, update: func }
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

以上三个函数每个if分支都是根据Vue提供的feature来进行不同的处理,其根本目的就是为了使传入的参数统一。如果你对哪个分支还有疑惑,可以去阅读下相关的官方文档。propsinjectdirectives


我们回到mergeOptions函数继续往下看,这里判断没有_base属性的话(被合并过不再处理,只有合并过的选项会带有_base属性),处理子选项的extend、mixins,处理方法就是将extend和mixins再通过mergeOptions函数与parent合并,因为mergeOptions函数合并后会返回新的对象,所以这时parent已经是个崭新的对象啦。

if (!child._base) {
  // 如果有extends属性(extends: xxx),则还是调用mergeOptions函数返回的结果赋值给parent
  if (child.extends) {
    parent = mergeOptions(parent, child.extends, vm)
  }
  // 如果有mixins属性(mixins: [xxx, xxx])
  // 则遍历数组,递归调用mergeOptions,结果也赋值给parent
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
}

接下来的最后一段代码如下:

// 定义options为空对象,最后函数返回结果是options
const options = {}
let key
// 先遍历parent执行mergeField
for (key in parent) {
  mergeField(key)
}
// 再遍历child,当parent没有key的时候,在执行mergeField。
// 如果有key属性,就不需要合并啦,因为上一步已经合并到options上了
for (key in child) {
  if (!hasOwn(parent, key)) {
    mergeField(key)
  }
}
// 该函数主要是通过key获取到对应的合并策略函数,然后执行合并,赋值给options[key]
function mergeField (key) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}
return options

到最后可以知道,mergeOptions函数进行真正的合并是最后一段代码,前面都是对选项进行规范化,以及extendmixins进行递归合并。那strats是啥呢?其实它是文件顶部定义的一个对象,它是config.optionMergeStrategies的引用,并且在之后对特殊的合并策略进行了重写,比如说eldata钩子函数componentspropsmethods等等。合并策略相关的代码我们在下一篇进行分析。

参考