【Vue源码】第十二节组件化之认识resolveConstructorOptions和mergeOptions

resolveConstructorOptions

mergeOptions方法的调用在_init中,我们从之前了解到_init是在new Vue时,这个一般发生在两个地方:

  • 外部代码,即我们自己new Vue;
  • 子组件内部调用new Vue
Vue.prototype._init = function (options?: Object) {
  // 如果是子组件
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
   // 否则
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
}

我们这次先来学习resolveConstructorOptions:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)

我们来分析一下resolveConstructorOptions

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  // 有super属性,说明Ctor是Vue.extend构建的子类
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions // Vue构造函数上的options,如directives,filters,....
    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
}

这个方法要分成两种情况来说明,第一种是Ctor是基础Vue构造器的情况,另一种是Ctor是通过Vue.extend方法扩展的情况。

Ctor是vue构造器时

Ctor是基础Vue构造器时,如通过new关键字来新建Vue构造函数的实例。

const vm = new Vue({
  el: '#app',
    data: {
      message: 'Hello Chris'
    }
})

此时resolveConstructorOptions就是直接返回options

// 这个时候options就是Vue构造函数上的options,是在initGlobalAPI(Vue)定义的
let options = Ctor.options
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

export function initGlobalAPI (Vue: GlobalAPI) {
  // ...
  Vue.options = Object.create(null)
  // 相当于
  // Vue.options.components = {}
  // Vue.options.directives = {}
  // Vue.options.filters = {}
  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
    
  // Vue 的内置组件目前有 <keep-alive>、<transition> 和 <transition-group> 组件
  // 把一些内置组件扩展到 Vue.options.components
  extend(Vue.options.components, builtInComponents)
  // ...
}

所以这时候返回的options是这样的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9TDWjMQc-1588666049968)(Vue 源码解读笔记.assets/5af660430001035410420172.png)]

Ctor是通过Vue.extend创建的子类
export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  // 有super属性,说明Ctor是Vue.extend构建的子类
  if (Ctor.super) {
    // 首先递归调用resolveConstructorOptions方法,返回"父类"上的options并赋值给superOptions变量
    const superOptions = resolveConstructorOptions(Ctor.super)
    // 然后把"自身"的options赋值给cachedSuperOptions变量
    const cachedSuperOptions = Ctor.superOptions // Vue构造函数上的options,如directives,filters,....
    // 然后比较这两个变量的值,当这两个变量值不等时,说明"父类"的options改变过了
    if (superOptions !== cachedSuperOptions) {
      // 例如执行了Vue.mixin方法,这时候就需要把"自身"的superOptions属性替换成最新的
      Ctor.superOptions = superOptions
      // 之后检查是否"自身"的options是否发生变化
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // 如果”自身“有新添加的options,则把新添加的options属性添加到Ctor.extendOptions属性上
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
     // 调用mergeOptions方法合并"父类"构造器上的options和”自身“上的extendOptions
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  // 返回合并后的options
  return options
}

function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  let modified // 定义modified变量
  const latest = Ctor.options // 自身的options
  const extended = Ctor.extendOptions // 构造"自身"时传入的options
  const sealed = Ctor.sealedOptions // 执行Vue.extend时封装的"自身"options,这个属性就是方便检查"自身"的options有没有变化
 // 遍历当前构造器上的options属性,如果在"自身"封装的options里没有,则证明是新添加的。执行if内的语句。调用dedupe方法,最终返回modified变量(即”自身新添加的options“)
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = dedupe(latest[key], extended[key], sealed[key])
    }
  }
  return modified
}

// lateset表示的是"自身"新增的options。extended表示的是当前构造器上新增的extended options,sealed表示的是当前构造器上新增的封装options
function dedupe (latest, extended, sealed) {
  // 防止生命周期构造函数重复
  // 如果latest不是数组的话(lateset是"自身"新增的options),这里不需要去重,直接返回latest。如果传入的latest是数组(如果latest是数组,一般这个新增的options就是生命周期钩子函数),则遍历该数组,如果该数组的某项在extended数组中有或者在sealed数组中没有,则推送到返回数组中从而实现去重。
  if (Array.isArray(latest)) {
    const res = []
    sealed = Array.isArray(sealed) ? sealed : [sealed]
    extended = Array.isArray(extended) ? extended : [extended]
    for (let i = 0; i < latest.length; i++) {
      // push original options and not sealed options to exclude duplicated options
      if (extended.indexOf(latest[i]) >= 0 || sealed.indexOf(latest[i]) < 0) {
        res.push(latest[i])
      }
    }
    return res
  } else {
    return latest
  }
}

总结

resolveConstructorOptions方法中判断是不是子构造器,不是的话直接返回Vue.opstions,是的话,递归调用resolveConstructorOptions合并组件的optionsVue上。

mergeOptions

mergeOptions方法的调用在_init中,我们从之前了解到_init是在new Vue`时,这个一般发生在两个地方:

  • 外部代码,即我们自己new Vue;
  • 子组件内部调用new Vue
Vue.prototype._init = function (options?: Object) {
  // 如果是子组件
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
   // 否则
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
}

外部调用,直接执行mergeOptions,我们来分析一下mergeOptions:

// 把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 来检查 options.components 组件名称是否合法:
  // 包含数字,字母,下划线,连接符,并且以字母开头
  // 是否和html标签名称或svg标签名称相同
  // 是否和关键字名称相同,如undefined, infinity等
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }
  // 如果child是function类型的话,我们取其options属性作为child
  if (typeof child === 'function') {
    child = child.options
  }
  // 进行规范化操作, 分别是把options中的props,inject,directives属性转换成对象的形式
  // 因为有些传入的时候可能会是数组的形式, 如 props: ['postTitle']
  // 分析normalizeProps
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
    
  // 先递归把 extends 和 mixins 合并到 parent 上
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  const options = {}
  let key
  // 然后遍历 parent,调用 mergeField
  for (key in parent) {
    mergeField(key)
  }
  // 然后再遍历 child
  for (key in child) {
    // 如果 key 不在 parent 的自身属性上,则调用 mergeField
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    // defaultStrat的逻辑是,如果child上该属性值存在时,就取child上的该属性值,如果不存在,则取parent上的该属性值
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
// normalizeProps(child, vm)
function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  // 当props是数组的时候
  // 把数组的每一项的值作为res对象的key,value值等于{type: null}
  // props: ['postTitle']
  // 转为 postTitle: { type: null }
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    // 当props是对象时
    // 遍历对象,先把对象的key值转换成驼峰的形式。
    // 然后再判断对象的值,如果是纯对象(即调用object.prototype.toString方法的结果是[object Object]),则直接把对象的值赋值给res,
    // 如果不是,则把{ type: 对象的值}赋给res
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    // 如果传入的props不是纯对象也不是数组,且当前环境也不是生产环境,则抛出警告
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

在创建组件的时候,我们先来回顾一下,由于组件的构造函数是通过 Vue.extend 继承自 Vue 的,先回顾一下这个过程,代码定义在 src/core/global-api/extend.js 中。

Vue.extend = function (extendOptions: Object): Function {

  Sub.options = mergeOptions(
    Super.options,
    extendOptions  // extendOptions 对应的就是前面定义的组件对象,它会和 Vue.options 合并到 Sub.opitons 中。
  )

  // ...
  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // ...
  return Sub
}
// 子组件的初始化过程,代码定义在 src/core/vdom/create-component.js 中
export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // vnode.componentOptions.Ctor 就是指向 Vue.extend 的返回值 Sub
  // 接着执行 this._init(options)
  // 因为 options._isComponent 为 true,那么合并 options 的过程走到了 initInternalComponent(vm, options)
  return new vnode.componentOptions.Ctor(options)
}
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // 这里的 vm.constructor 就是子组件的构造函数 Sub
  const opts = vm.$options = Object.create(vm.constructor.options)

  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

总结

对于 options 的合并有 2 种方式:

  • 执行外部 new Vue 时,会调用 mergeOptions 函数,并根据不同的选项调用不同的合并策略函数
  • 子组件实例化时,会调用 initInternalComponent 函数进行合并,比外部初始化 Vue 通过 mergeOptions 的过程要快,合并完的结果保留在 vm.$options 中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值