Vue2 源码解读二:mergeOptions

文章目录

Vue2 源码解读一:Global API
Vue2 源码解读二:mergeOptions
  1、入口方法
  2、检查组件名称的合法性
  3、数据规范化、格式化
  4、合并策略
Vue2 源码解读三:Vue实例
Vue2 源码解读四:Observer模块


Vue2 源码解读二:mergeOptions

mergeOptions是Vue的源码中常用的合并策略,对Vue的data、methods、computed、watch、lifecycle等等的合并。在Vue实例化、Vue.extend、Vue.mixin外部调用方法中都有调用。

1、入口方法

该方法可分为以下几个步骤:

  1. 检查组件名称的合法性checkComponents
  2. 数据规范化、格式化normalizeProps()、normalizeInject()、normalizeDirectives()
  3. 处理extend、mixinchild.extends、child.mixins
  4. 合并字段mergeField,按照不同的属性采用不同的合并策略
/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 * 合并两个options对象,并返回一个新的options
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    // 检查组件名称的合法性
    checkComponents(child)
  }

  // 兼容处理child为function的情况
  if (typeof child === 'function') {
    child = child.options
  }
  
  // 规范化、格式化Props
  normalizeProps(child, vm)
  // 规范化、格式化Inject
  normalizeInject(child, vm)
  // 规范化、格式化Directives
  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.
  // _base来自于Vue初始化
  if (!child._base) {
  	// 处理extends
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
  	// 处理mixins
    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
  // 把parent合并到options
  for (key in parent) {
    mergeField(key)
  }
  // 再把child合并到options
  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
}

2、检查组件名称的合法性

使用正则判断组件名称的合法性,然后再判断是否为内置标签。

/**
 * Validate component names
 */
function checkComponents (options: Object) {
  // 检查每一个组件的名称
  for (const key in options.components) {
    validateComponentName(key)
  }
}

export function validateComponentName (name: string) {
  // 正则校验
  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.'
    )
  }
  // 是否为内置标签,比如slot、component
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }

3、数据规范化、格式化

把props、inject、directives转成标准格式的对象。

/**
 * Ensure all props option syntax are normalized into the
 * Object-based format.
 * 格式化props,确保每个都是标准格式的对象
 */
function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  // 没有则不处理
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    // 如果prop是数组,则循环处理每一个prop
    i = props.length
    while (i--) {
      val = props[i]
      // props数组里面必须是string
      if (typeof val === 'string') {
        // “大驼峰”命名
        name = camelize(val)
        // 转成含有type字段的对象
        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)
      // 处理 只指定类型的prop
      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
}

/**
 * Normalize all injections into Object-based format
 * 格式化injections,确保每个都是标准格式的对象
 */
function normalizeInject (options: Object, vm: ?Component) {
  const inject = options.inject
  // 没有则不处理
  if (!inject) return
  // options.inject,在初始化的时候,已响应式,修改normalized,会同步修改options.inject
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      // 转成含有from字段的对象
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      // 转成含有from字段的对象
      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
    )
  }
}

/**
 * Normalize raw function directives into object format.
 * 格式化injections,确保每个都是标准格式的对象
 */
function normalizeDirectives (options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') {
        // 转成包含bind、update属性的对象
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

4、合并策略

Vue针对不同属性定义不同的合并策略,所有的策略方法基本都包含四个参数:parentVal,childVal,vm,key。

  1. 默认的合并策略:优先返回后者child,没有再返回前者parent
  2. el和propsData的合并策略:使用默认的合并策略
  3. data和provide的合并策略:如果data是一个function,则调用,使之返回一个对象。然后再合并递归data对象中的每一个字段,新字段添加监听属性。相同属性使用前者to,抛弃后者from。最后返回一个function。
  4. 钩子函数的合并策略:使用数组装载相同的钩子函数,并消除重复的钩子函数,后面按序触发调用
  5. component、filter、directive的合并策略:采用后者childVal,前者parentVal放在_proto_属性中。
  6. watch的合并策略:非覆盖,数组合并
  7. props、methods、inject、computed的合并策略:相同属性,后者childVal覆盖前者parentVal
/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 * 
 * option覆盖策略
 */
const strats = config.optionMergeStrategies

/**
 * Options with restrictions
 * el和propsData的合并策略,使用默认的策略
 */
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)
  }
}

/**
 * Helper that recursively merges two data objects together.
 * 合并data,把from对象合并到to对象,并返回to对象
 */
function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal

  // 获取from的所有key(包括Symbol)
  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...
    // 跳过__ob__属性
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]

    if (!hasOwn(to, key)) {
      // 如果to不包含key,则把from的值给to
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      // 如果fromVal和toVal都是对象,那么递归合并
      mergeData(toVal, fromVal)
    }
  }
  return to
}

/**
 * Data
 * 合并data,data可能是对象,也可能是function
 */
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
      }
    }
  }
}
// Vue中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)
}

/**
 * Hooks and props are merged as arrays.
 * 合并钩子函数,比如:created、mounted、beforeDestroy,非覆盖,数组合并
 */
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
}

// 钩子函数的合并策略
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

/**
 * Assets
 *
 * When a vm is present (instance creation), we need to do
 * a three-way merge between constructor options, instance
 * options and parent options.
 * 
 *  合并component、filter、directive
 */
function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  // 使用parent的_proto_,或者空对象
  const res = Object.create(parentVal || null)
  if (childVal) {
    // 只能是对象
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    // 对象属性覆盖处理
    return extend(res, childVal)
  } else {
    return res
  }
}

// component、filter、directive 合并策略
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

/**
 * Watchers.
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 * 
 * watch的合并策略,非覆盖,数组合并
 */
strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // work around Firefox's Object.prototype.watch...
  // 火狐浏览器的Object.prototype.watch兼容处理
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  // 如果childVal不存在,则返回parentVal的_proto_的空对象
  if (!childVal) return Object.create(parentVal || null)
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  // 如果parentVal不存在,则返回childVal
  if (!parentVal) return childVal
  const ret = {}
  // 复制一份parentVal
  extend(ret, parentVal)

  // 数组化,有则concat,没有则创建。
  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
}

/**
 * Other object hashes.
 * 
 * props、methods、inject、computed的合并策略
 */
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // childVal必须是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
}
// provide 和data的合并策略一样
strats.provide = mergeDataOrFn

/**
 * Default strategy.
 * 默认策略,优先child,在parent
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值