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),无论 vm 是 Vue 实例还是 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. 格式化 props,inject,directives
  2. 合并子选项 extends,mixins 到父选项
  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 的实现,对于不同的属性是调用不同的策略函数进行合并的。后面将继续分析组件化的实现。



相关文章
|
传感器 芯片
最详细STM32,cubeMX 点亮 led
最详细STM32,cubeMX 点亮 led
1409 0
|
关系型数据库 MySQL
Mysql连接无效(invalid connection)解决方案
Mysql连接无效(invalid connection)解决方案
2099 0
Mysql连接无效(invalid connection)解决方案
|
6月前
|
人工智能 小程序 API
阿里云计算巢OpenClaw一键部署实战|轻量服务器搭建+微信小程序对接+百炼免费大模型配置完整教程
2026年,开源AI行动框架OpenClaw(曾用名Clawdbot)凭借“自然语言驱动系统操作、全平台无缝集成”的核心优势,成为个人与团队效率提升的首选工具。本文完整覆盖**新手零基础阿里云轻量服务器部署OpenClaw(Clawdbot)简单步骤及免费大模型阿里云百炼Coding Plan API配置及避坑指南**,同步新增阿里云计算巢秒级部署、微信小程序全流程集成方案,全程提供可直接复制的代码命令、可视化操作指引与高频问题排查方案,确保零基础用户15分钟内完成部署、稳定运行、全能力打通,快速拥有专属AI数字助手。
618 7
|
6月前
|
SQL 测试技术 数据库
SELECT * 到底能不能用?新手第一个坑
拆解“SQL学会SELECT *就够了”的误区,详解其原理、初学爽感与三大隐患(性能崩塌、列序失控、资源浪费),并指出仅限探索/排查等有限场景可用。真进阶在于懂学会何时不用它!
|
API 开发者
提供一份 1688 商品详情接口的错误码及解决方法
本文介绍了 1688 商品详情接口常见的错误码及其解决方法,包括 401(未授权)、403(禁止访问)、404(未找到)、429(请求过多)和 500/502/504(服务器错误),帮助开发者快速定位并解决问题。
|
机器学习/深度学习 人工智能 自动驾驶
AI技术在现代生活中的应用:从理论到实践
AI技术在现代生活中的应用:从理论到实践
1474 2
|
JavaScript 前端开发 架构师
Node框架 【Egg-企业级框架】
Node框架 【Egg-企业级框架】
1066 0
|
缓存 监控 数据处理
【编程底层原理】从播放音乐的网页中提取mp3音频文件的两种方式及背后的技术思考【短连接和长连接】
本文介绍了两种从网页提取音乐文件的方法:一是通过IE临时缓存获取,二是利用开发者模式捕捉网络流量并下载音频URL。同时探讨了网页播放音乐的技术实现,包括短连接和长连接的区别及其适用场景,以及数据传输中的阻塞概念。
5688 0
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的校园疫情管控系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的校园疫情管控系统附带文章和源代码部署视频讲解等
337 8
|
人工智能
AI绘画——ControlNet扩展安装教程
AI绘画——ControlNet扩展安装教程
2814 0

热门文章

最新文章