vue2-选项合并策略

506 阅读3分钟

前面我们学习了 vue实例化过程,在其中有这么个过程 mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm),我们今天来重点梳理下 mergeOptions

resolveConstructorOptions

我们先来看看 resolveConstructorOptions(vm.constructor),这边入参为实例的构造函数

new Vue 实例化为例子,此时的构造函数就是 Vue

export function resolveConstructorOptions (Ctor: Class<Component>) {
  // 这边主要就是返回Ctor.options
  let options = Ctor.options

  // 跳过super option changed的情况
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

那么 Vue.options 又是在哪定义的呢,

core/global-api/index.js 中的 initGlobalAPI 能找到 options 初始化

Vue.options = Object.create(null)

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

Vue.options._base = Vue

可见这边主要就是初始化了 component, directive, filter,当我们在函数中调用 Vue.component, Vue.directive, Vue.filter 为注册全局资源时就会往 Vue.options 中注入对应资源。

Vue.extend

细心的朋友在这边可能会发现 resolveConstructorOptions(vm.constructor),其中的 vm.constructor 并不一定是 Vue,有时候会是 VueComponent,那这时 Ctor.options 又是啥呢?

其实在 new Vue 之后,遇到的组件并不会直接调用 new Vue 来初始化,而是会调用 Vue.extend 来注册组件,其代码在 core/global-api/extend.js,我们可以看看部分代码

  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this

    // ...
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // ...
    return Sub
  }
}

可以发现 Sub 其实是个新的函数,其原型继承自 Super.prototype,而且其静态方法 options 也是来自 Vue.options

所以回到上面 resolveConstructorOptions(vm.constructor),无论 vmVue 实例还是 VueComponent 实例,其实都是指向 Vue.options

mergeOptions

前面分析可知 resolveConstructorOptions(vm.constructor) 主要就是返回了 Vue.options,我们现在进入今天的重点 mergeOptions,其位于 core/util/options/js

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 开放环境校验组件名称
  // 为什么只校验child?因为parent已经校验过了
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  // 兼容写法
  if (typeof child === 'function') {
    child = child.options
  }

  // 这边有几个normalize分别对Props Inject Directives 配置进行格式化处理
  // 例如Props的属性会被修改为驼峰式 Directives中的函数写法会格式化为对象
  // 具体的大家可以去了解下
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // 将extends mixins 的配置合并到 parent
  // 注意这边的策略是先让父选项merge而不是子选项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)
      }
    }
  }

  // 重点部分
  // 定义输出值options={}
  const options = {}
  let key

  // 合并parent中有的选项
  // 这边稍不留神就容易入坑
  // 得留意这边往mergeField传入得是key而不是parent中对应得value
  // 本质上是合并两者
  for (key in parent) {
    mergeField(key)
  }

  // 合并仅child中有的选项
  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 函数的整体流程比较清晰

  1. 格式化 propsinjectdirectives

  2. 合并子选项 extendsmixins 到父选项

  3. 调用 mergeField 合并父子选项

合并策略

前面我们分析 mergeOptions 中通过 mergeField 来合并选项,而 mergeField 也比较简单,就是根据不同的合并属性来调用不同的 strats[key](),并将父子属性值传入。其中的 strats 是我们分析的重点,我们称其为 策略对象,不同的 key-value 表示对不同的属性有不同的合并策略函数

// 初始值一般为空对象{}
const strats = config.optionMergeStrategies

options.js 中为其初始化了不同的 策略函数

默认策略

有子选项则返回子选项,否则返回父选项

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

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

lifeCycleHooks

调用 concat 合并生命周期钩子数组,同时会将子数据格式化为数组,所以在q全局中通过 Vue.mixin 的生命周期会合并到组件生命周期中,依次调用

// LIFECYCLE_HOOKS = ['beforeCreate', 'created', 'beforeMount', 
// 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed',
// 'activated', 'deactivated', 'errorCaptured', 'serverPrefetch']
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

// 调用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
}

// 删除重复钩子
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
}

assets

返回子对象和父对象合并的值,其中子选项会覆盖父选项

// ASSET_TYPES = ['component', 'directive', 'filter']
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    // assertObjectType 检查是否为对象类型
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

props/methods/inject/computed

assets 差不多,返回子对象和父对象合并的值,其中子选项会覆盖父选项

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
}

watch

会先判断是否为浏览器对象原型属性,后面就是合并父子选项,其中合并策略是通过 concat 合并数组,其中会先判断父子选项是否为数组最终格式化为数组

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
}

data

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    // 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
    }

    // 调用mergeDataOrFn
    return mergeDataOrFn(parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  // 会分为有vm和无vm的情况
  // 主要区别在于call中绑定的this
  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) {
        // 最终值通过mergeData来合并
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

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
      mergeData(toVal, fromVal)
    }
  }
  return to
}

data 的合并策略比较复杂一些,我们来总结一下

  1. 检查 data 选项是否为函数类型,否在抛出警告

  2. 调用 mergeDataOrFn 返回新函数,其中新函数中调用 mergeData 合并父子选项

  3. mergeData 中将递归遍历父数据,将其拷贝到子数据中

可以发现对 data 的合并来说,其会进行递归合并

总结

本篇文章主要梳理了在组件实例化 _init 中,对于配置选项的合并 vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm) 是如何进行的。其中主要分析了 mergeOptions 的实现,对于不同的属性是调用不同的策略函数进行合并的。后面将继续分析组件化的实现。