Vue2源码学习笔记 - 5.options选项合并

上一节我们学习了 Vue 应用实例的初始化过程,其中有很多细节仍需我们去研读,这一节我们就先来研究分析 options 选项的合并过程。

我们继续回到 _init 的代码段,它在文件 /src/core/instance/init.js 中,这个方法在应用和组件实例化时都是必须调用的,options 选项合并就在这里被执行。

// Vue.prototype._init 方法代码段
...
// merge options
if (options && options._isComponent) {
  // 组件实例化时合并选项
  initInternalComponent(vm, options)
} else {
    // Vue 应用实例化时合并选项
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}
...

选项合并在两个情况下都会发生,从代码分支语句看,一个是常见的 new Vue(options) 的时候,另一个情况就是实例化组件的时候。我们先看 new Vue 实例化时的合并过程,它先调用 resolveConstructorOptions(vm.constructor),相当于直接调用了 resolveConstructorOptions(Vue),这个函数在这个情况下是直接返回 Vue.options 的。

那么,有其他情况吗?有,那就是在创建组件的 Vnode 节点的时候,调用它重新合并组件选项,这个我们后面说。继续先看看 Vue.options,它先在 /src/core/global-api/index.js 中定义

// /src/core/global-api/index.js
...
Vue.options = Object.create(null)
// ASSET_TYPES 为常量数组
// const ASSET_TYPES = ['component','directive','filter'] <-----
ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})

// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue
// builtInComponents = KeepAlive 组件
extend(Vue.options.components, builtInComponents)
...

设置 Vue.options 空对象,然后遍历 ASSET_TYPES 添加属性 component,directive,filter,然后把内置组件 KeepAlive 合并到 Vue.options.components 上。之后在 /src/platforms/web/runtime/index.js 中合并平台相关的组件

...
import platformDirectives from './directives/index'
import platformComponents from './components/index'
...
// install platform runtime directives & components
// 合并 平台相关 的指令(v-model,v-show)
extend(Vue.options.directives, platformDirectives)
// 合并 平台相关 的组件(Transition, TransitionGroup)
extend(Vue.options.components, platformComponents)
...

这里导入的 platformDirectives 为 web 平台下的指令,如:v-show,v-model;platformComponents 为 web 平台下的内置组件,如:Transition, TransitionGroup。最后,在使用 Vue.component 和 Vue.directive 注册组件和指令时也会分别写入 Vue.options.components 和 Vue.options.directives 对象里。那么最终,Vue.options 大概就是这样:

Vue.options = {
  components: {
    Blog: ƒ VueComponent(options), // 自定义组件
    Hello: ƒ VueComponent(options),// 自定义组件
    Test: ƒ VueComponent(options), // 自定义组件
    // Vue 内置组件
    KeepAlive: {name: 'keep-alive', abstract: true, props: {}, methods: {}, created: ƒ,},
    Transition: {name: 'transition', props: {}, abstract: true, render: ƒ},
    TransitionGroup: {props: {}, methods: {}, beforeMount: ƒ, render: ƒ, updated: ƒ}
  },
  directives: {
      // 内置指令
    model: {inserted: ƒ, componentUpdated: ƒ}, // v-model
    show: {bind: ƒ, update: ƒ, unbind: ƒ} // v-show
  },
  filters: {},
  _base: ƒ Vue(options) // Vue.options._base === Vue
}

这个 Vue.options 和 实例化传入的 options 再传给 mergeOptions,它的定义在文件 /src/core/util/options.js 中。

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    // 遍历检测 options.components 的组件名
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }
  // 过滤并格式化 options.props 属性为 对象格式保存
  normalizeProps(child, vm)
  // 过滤并格式化 options.inject 属性为 对象格式保存
  normalizeInject(child, vm)
  // 格式化纯函数指令 options.directives 为 对象格式保存
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      // 扩展属性 options.extends 与父 options 合并
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      // 混入属性 options.mixins 与父 options 合并
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  // 根据 parent 中的 key 调用 mergeField 合并选项
  for (key in parent) {
    mergeField(key)
  }
  // 根据在 child 中且不在 parent 中的 key 继续 调用 mergeField 合并选项
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  // 从 strats 中获取具体的选项合并函数执行合并操作
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

在 mergeOptions 中先是检查 options.components 里的组件名,后面过滤处理 props,inject,directives,然后合并 extends 和 mixins。最后先遍历 parent 选项,再遍历 child 选项,调用 mergeField 并把 key 传入,进行合并操作。来看看函数 mergeField 中 strats 的定义:

const strats = config.optionMergeStrategies

if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    ...
  }
}

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  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) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

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

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
}

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

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

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

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
}

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
}
strats.provide = mergeDataOrFn

那么 strats 最终看起来应该就是这样

strats = {
  activated: ƒ mergeHook( parentVal, childVal ),
  beforeCreate: ƒ mergeHook( parentVal, childVal ),
  beforeDestroy: ƒ mergeHook( parentVal, childVal ),
  beforeMount: ƒ mergeHook( parentVal, childVal ),
  beforeUpdate: ƒ mergeHook( parentVal, childVal ),
  components: ƒ mergeAssets( parentVal, childVal, vm, key ),
  computed: ƒ ( parentVal, childVal, vm, key ),
  created: ƒ mergeHook( parentVal, childVal ),
  data: ƒ ( parentVal, childVal, vm ),
  deactivated: ƒ mergeHook( parentVal, childVal ),
  destroyed: ƒ mergeHook( parentVal, childVal ),
  directives: ƒ mergeAssets( parentVal, childVal, vm, key ),
  el: ƒ (parent, child, vm, key),
  errorCaptured: ƒ mergeHook( parentVal, childVal ),
  filters: ƒ mergeAssets( parentVal, childVal, vm, key ),
  inject: ƒ ( parentVal, childVal, vm, key ),
  methods: ƒ ( parentVal, childVal, vm, key ),
  mounted: ƒ mergeHook( parentVal, childVal ),
  props: ƒ ( parentVal, childVal, vm, key ),
  propsData: ƒ (parent, child, vm, key),
  provide: ƒ mergeDataOrFn( parentVal, childVal, vm ),
  serverPrefetch: ƒ mergeHook( parentVal, childVal ),
  updated: ƒ mergeHook( parentVal, childVal ),
  watch: ƒ ( parentVal, childVal, vm, key )
}

那么在 mergeField 中对于每个 options 的 key 调用 strats 中对应的合并函数来执行合并操作,比如 filters 选项调用 mergeAssets 合并,created 等生命钩子选项调用 mergeHook 合并,还有 data\el\props\computed 等也调用相应的函数合并。
Vue选项合并流程

总结:

这里我们主要详细学习 new Vue 应用实例化的选项合并策略,简而言之就是合并 Vue.options 与 new Vue(options) 中的 options 为新的选项对象并保存在实例 vm 的 $options 属性上。合并的具体策略是函数 mergeField 调用 strats 中定义的合并函数并返回,如上图所示。组件的选项合并分了两个阶段,我们在学习组件的相关知识点时在详细介绍。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值