vue2-选项合并策略

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

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



相关文章
|
2月前
|
JavaScript
在 Vue 中处理组件选项与 Mixin 选项冲突的详细解决方案
【10月更文挑战第18天】通过以上的分析和探讨,相信你对在 Vue 中使用 Mixin 时遇到组件选项与 Mixin 选项冲突的解决方法有了更深入的理解。在实际开发中,要根据具体情况灵活选择合适的解决方案,以确保代码的质量和可维护性。
110 7
|
7月前
|
JavaScript
Vue组件选项编写代码的特点和注意事项
Vue组件选项编写代码的特点和注意事项
41 2
|
7月前
|
索引
第19节:Vue3 在模板中展开时的注意事项
第19节:Vue3 在模板中展开时的注意事项
50 0
vue3 element-plus 实现表格数据更改功能
在 vue3 中使用 element-plus 实现表格数据更改功能,可以通过以下步骤实现:
1183 0
|
2月前
|
缓存 JavaScript 前端开发
Vue 中动态导入的注意事项
【10月更文挑战第12天】 在 Vue 项目中,动态导入是一种常用的按需加载模块的技术,可以提升应用性能和效率。本文详细探讨了动态导入的基本原理及注意事项,包括模块路径的正确性、依赖关系、加载时机、错误处理、缓存问题和兼容性等,并通过具体案例分析和解决方案,帮助开发者更好地应用动态导入技术。
|
4月前
|
JavaScript 前端开发
在Vue3+ElementPlus项目中实现一个简单的新增/移除行记录的小组件
在Vue 3和Element Plus项目中创建一个支持新增和移除行记录的简单表格组件。
330 0
|
7月前
|
JavaScript
组件中写选项的顺序(vue的问题)
组件中写选项的顺序(vue的问题)
37 0
|
JavaScript
原来这么简单!Vue实现动态表头详细步骤
原来这么简单!Vue实现动态表头详细步骤
244 0
|
JavaScript
Vue 全局导入 JS 方式以及对 ClassName 进行增删扩展
Vue 全局导入 JS 方式以及对 ClassName 进行增删扩展
83 0
|
7月前
|
小程序 数据安全/隐私保护
Vue.directive指令实现按钮级别权限控制
Vue.directive指令实现按钮级别权限控制