(二)小菜鸡的Vue源码阅读——说说Vue实例的options合并策略

411 阅读2分钟

0.前情提要:

Vue.prototype._init = function (options?: Object) {
    // ...
    vm.$options = mergeOptions(
        // 该函数会沿着原型链往上找到有options属性的地方
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
    // ...
}

之前已经看到new Vue的时候是在执行_init方法,在_init中合并了传入的options,本文具体看看options是如何被合并的

1.mergeOptions函数

找到core/instance/util/options.js,大概做了以下工作

  1. 检测名称name
  2. 统一options输入属性的类型(props,inject,directives
  3. extends,mixins递归合并
  4. 按照定义好的不同属性的合并策略进行合并
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
  }
  // 将props声明 统一格式化为对象{[key: string]:{type: 'string',default?:any}}
  normalizeProps(child, vm)
  // 和props差不多的逻辑
  normalizeInject(child, vm)
  // 直接声明函数的时候转化为{bind: def,update: def}
  normalizeDirectives(child)

 
  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
}

1.1 格式化 props、inject、directives

normalizePropsprops的声明有两种方式

  1. 数组声明 如props: ['a','b'],元素是props属性名
  2. 对象声明 如props:{a:{type:"string",default},b:"string"} 最终转换的格式应该是{[key: string]:{type: string,default?: any}}
// 简化后的
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]
      // w-w 转为驼峰命名
      name = camelize(val)
      // 增加类型校验
      res[name] = { type: null }
    }
  // 对象形式的声明
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      // w-w 转为驼峰命名
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        // 对象声明的时候,如果属性值不是对象,那么属性值就代表类型
        : { type: val }
    }
  }
  options.props = res
}

normalizeInject

function normalizeInject (options: Object, vm: ?Component) {
  const inject = options.inject
  if (!inject) return
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
    }
  }
}

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 合并child中的extends、mixins

一个mergeOptions的递归调用,合并之后修改parent

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.3 合并child和parent的并集

不同的属性有不同的合并策略,列举一下

  1. elpropsData
  2. data
  3. 各个生命周期hook(来自LIFECYCLE_HOOKS
  4. componetsdirectivefilter来自(ASSET_TYPES
  5. watch
  6. propsmethodsinjectcomputed
  7. provide

elpropsData(合并覆盖)

实际上就是用的默认合并策略,只不过在开发环境下做了容错警告

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

那么看看defaultStrat,就是如果子集存在,把父级覆盖,这里其实就可以回想到extendsmixins的合并,因为合并之后赋给了parent此时在合并属性的时候,extendsmixins中的属性会被child中的覆盖(这里指的是用默认合并策略的时候)

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

data(对象的递归合并覆盖)

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // 要求child的data属性是一个函数
    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)
}

mergeDataOrFn

export function mergeDataOrFn (parentVal: any,childVal: any,vm?: Component): ?Function {
  if (!vm) {
    // 两个中有一个不存在就返回另一个
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
   
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    // 考虑到data属性是对象或函数的情况 
    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
      }
    }
  }
}

mergeData

这里有种情况说明一下,如果parentchild的值不全是对象,那么就会child覆盖parent,因为没法合并

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal

  // 获取到父data的属性名
  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  // 对父data的属性做循环
  for (let i = 0; i < keys.length; i++) {
    // 获取属性名
    key = keys[i]
    // 跳过__ob__
    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
}

hook(数组合并,先父后子,去重)

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

// shared/constant.js
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

mergeHook 这个三目运算符嵌套看着有点晕,还是解释一下吧

  1. child不存在,那直接用`parent
  2. child存在,parent不存在,那就用child(如果child不是数组就强行变数组[child]
  3. child存在,parent也存在,parent直接连接childparent.concat(child)注意这里concat既可以连接数组,也能连接单个元素,这样的连接方式也决定了调用顺序,先父后子
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
}

dedupeHooks去重

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
}

componetsdirectivefilter(合并覆盖)

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

// shared/constant.js
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

mergeAssets

function mergeAssets (parentVal: ?Object,childVal: ?Object,vm?: Component,key: string): Object {
  // 把parent放到原型链上
  const res = Object.create(parentVal || null)
  if (childVal) {
    // 规定传值类型为对象
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

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

watch(合并覆盖,强制数组封装)

strats.watch = function (parentVal: ?Object,childVal: ?Object,vm?: Component,key: string): ?Object {
  // 好像是火狐的Object.prototype上有watch这个属性,所以需要判断过滤一下
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  
  // 子不存在直接返回父
  if (!childVal) return Object.create(parentVal || null)
  // 验证需要是对象
  if (process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  // 父不存在返回子
  if (!parentVal) return childVal
  const ret = {}
  // 都存在就合并覆盖
  // 每一个watch属性下都是数组
  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
}

propsmethodsinjectcomputed(合并覆盖)

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
}

provide(同data

strats.provide = mergeDataOrFn

2.简单总结

  1. 说是和parentchild的合并,实际上很重要的一点是mixinsextends合并也已经在这里进行了,而且mixins的合并规则好像经常被单独拎出来会出面试
  2. 合并的过程,伴随着数据结构的格式化(通一个属性,在使用的时候允许多种数据类型输入方式,但是内部处理的时候肯定是要转换统一的),这个应该为了后面的处理做准备的。
  3. 有意思的是创建对象的时候用Object.creat(null),这样会省去Object原型链的挂载,比字面量创建对象看起来简洁舒服,学到了~ 4.通过strats对象定义不同的合并策略实际上就是用了设计模式里的策略模式

下一节可能会根据_init函数initState函数进行阅读,未完待续~