vue2-选项合并策略

前面我们学习了 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 的实现,对于不同的属性是调用不同的策略函数进行合并的。后面将继续分析组件化的实现。

猜你喜欢

转载自juejin.im/post/7034834945293942814