Skip to content

合并配置

new Vue的场景有两个,一个是主动调用new Vue(options) 的方式实例化一个 Vue 对象;另一种组件创建过程中内部通过 new Vue(options) 实例化子组件Sub。它们都会执行实例的_init(options)方法,首先会执行merge options,不过这里有一个判断是如果存在options._isComponent,也就是这里分场景去合并了,

js
 Vue.prototype._init = function (options?: Object) {
    ...
   // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // 合并用户传入的options和vue的构造函数的options
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
 }

一个简单的示例:

js
import Vue from 'vue'

let childComp = {
  template: '<div>{{msg}}</div>',
  created() {
    console.log('child created')
  },
  mounted() {
    console.log('child mounted')
  },
  data() {
    return {
      msg: 'Hello Vue'
    }
  }
}

Vue.mixin({
  created() {
    console.log('parent created')
  }
})

let app = new Vue({
  el: '#app',
  render: h => h(childComp)
})

外部调用

当执行 new Vue 的时候,在执行 this._init(options) 的时候,就会执行如下逻辑去合并 options,通过调用 mergeOptions 方法来合并,它实际上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并,resolveConstructorOptions 的实现先不考虑,在我们这个场景下,它返回的是 vm.constructor.options,相当于 Vue.options,这个值在 initGlobalAPI(Vue) 的时候定义了,之前也分析过

js
export function initGlobalAPI (Vue: GlobalAPI) {
  // ...
  Vue.options = Object.create(null)
  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

  extend(Vue.options.components, builtInComponents)
  // ...
}

这部分在init一节我们详细分析过,最后通过extend将一些内置组件扩展到Vue.options.components上。

回到mergeOptions,详细看下合并策略。

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') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  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
  for (key in parent) {
    mergeField(key)
  }
  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主要就是将parent和child两个对象根据合并策略来合并成一个新对象返回。首先将child的extends 和 mixins 合并到 parent 上,然后遍历parent,调用 mergeField,然后再遍历 child,如果 key 不在 parent 的自身属性上,则调用 mergeField。

重点看下mergeField方法,它对于不同的key将采用不同的合并策略。 比如生命周期函数,

js
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

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

这其中的 LIFECYCLE_HOOKS 的定义在 src/shared/constants.js 中:

js
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]

这里使用了三层的3元运算符,如果不存在childVal,则直接返回parentVal。如果存在,则继续判断parentVal是否存在,如果存在,合并parentVal和childVal,如果不存在,则保证childVal是一个数组并返回。所以一旦 parent 和 child 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。 最后vm.$options 的值差不多是如下这样:

js
vm.$options = {
  components: { },
  created: [
    function created() {
      console.log('parent created')
    }
  ],
  directives: { },
  filters: { },
  _base: function Vue(options) {
    // ...
  },
  el: "#app",
  render: function (h) {
    //...
  }
}

组件 merge

前面说过,组件的构造函数Ctor通过Vue.extend创建,定义在src/core/global-api/extend.js中,

js
/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
  // ...
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )

  // ...
  // 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
}

extendOptions会和Super.options合并到Sub.options中。

当实例化子组件时,接下来我们再看下子组件的初始化过程,代码定义在 src/core/vdom/create-component.js,

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
  }
  // ...
  return new vnode.componentOptions.Ctor(options)
}

这个函数是在初始化函数init中调用的的,而init这个钩子函数在创建组件createComponent中安装,createComponent中比较重要的一部分就是安装组件钩子函数。而createComponentInstanceForVnode就是init中调用

js
init(){
  ...
   const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  ...
}

createComponentInstanceForVnode也定义在src/core/vdom/create-component.js

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
  }
  // ...
  return new vnode.componentOptions.Ctor(options)
}

最后返回了new vnode.componentOptions.Ctor(options),vnode.componentOptions.Ctor 就是通过Vue.extend返回的Sub,就会执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑,

js
  const Sub = function VueComponent (options) {
    this._init(options)
  }

因为 options._isComponent 为 true,那么合并 options 的过程走到了 initInternalComponent(vm, options) 逻辑。分析到这里,不得不感叹一下这缜密的逻辑,一切有迹可循,需要学习的是其中的设计思想。 先来看一下它的代码实现,在 src/core/instance/init.js 中,

js
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  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
  }
}

先执行了const opts = vm.$options = Object.create(vm.constructor.options),vm.constructor就是Sub。 然后vm.$options中保存了父vnode实例包括父vnode的其他配置(propsData,listeners,children,tag),执行完之后, vm.$options中的配置如下:

js
vm.$options = {
  parent: Vue /*父Vue实例*/,
  propsData: undefined,
  _componentTag: undefined,
  _parentVnode: VNode /*父VNode实例*/,
  _renderChildren:undefined,
  __proto__: {
    components: { },
    directives: { },
    filters: { },
    _base: function Vue(options) {
        //...
    },
    _Ctor: {},
    created: [
      function created() {
        console.log('parent created')
      }, function created() {
        console.log('child created')
      }
    ],
    mounted: [
      function mounted() {
        console.log('child mounted')
      }
    ],
    data() {
       return {
         msg: 'Hello Vue'
       }
    },
    template: '<div>{{msg}}</div>'
  }
}