Vue源码解析之 合并配置

269 阅读2分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue 源码解析系列第 5 篇,关注专栏

前言

Vue 合并配置过程主要发生在 new Vue 初始化及组件创建时,初始化主要执行 _init 方法,它定义在 src/core/instance/init.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 {
    // 合并配置
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
}

下面我们通过案例,来分析下 Vue 的合并配置过程

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)
})

合并过程

上文我们可以看出,合并配置过程主要执行 mergeOptions 方法,该方法定义在 src/core/util/options.js

vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)
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 方法主要目的是把 parentchild 这两个对象根据一些合并策略,合并成一个新对象并返回。上述案例,vm.$options 合并后的值大致如下:

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

根据案例,先执行 Vue.mixin 方法,它被定义在 src/core/global-api/mixin.js,其主要目的是把 mixin 中的 created 方法混入到全局的 created 钩子函数中。

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    // 合并配置
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
// this.options 配置
{
    components: {KeepAlive: {…}, Transition: {…}, TransitionGroup: {…}}
    directives: {model: {…}, show: {…}}
    filters: {}
    _base: ƒ Vue(options)
}

// mixin 传入的对象
{
  created() {
    console.log('parent created')
  }
}

// 合并配置后
{
    components: {}
    created: [ƒ]
    directives: {}
    filters: {}
    _base: ƒ Vue(options)
}

接着会执行 new Vue 实例,该过程会执行初始化 _init ,即调用 mergeOptions 方法合并 app 定义的配置参数,之后会打印出 parent created

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

// resolveConstructorOptions(vm.constructor) 返回结果
{
    components: {}
    created: [ƒ]
    directives: {}
    filters: {}
    _base: ƒ Vue(options)
}

// options 参数
{
    el: "#app"
    render: h => h(childComp)
}

// 合并配置后
{
    components: {}
    created: [ƒ]
    directives: {}
    el: "#app"
    filters: {}
    render: h => h(childComp)
    _base: ƒ Vue(options)
}

之后会执行组件配置,由于组件的构造函数是通过 Vue.extend 继承自 Vue 的,它定义在 src/core/global-api/extend.js

Vue.extend = function (extendOptions: Object): Function {
  // 省略
  
  // 合并配置
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // 省略
  return Sub
}

// Sub.options 合并后配置如下
// 可以发现父定义了created,子定义了created,最终按先父后子顺序合并
{
    components: {}
    created: (2) [ƒ, ƒ]
    data: ƒ data()
    directives: {}
    filters: {}
    mounted: [ƒ]
    name: "ChildComp"
    template: "<div>{{msg}}</div>"
    _Ctor: {}
    _base: ƒ Vue(options)
}

Vue 组件化 文章中我们提到子组件初始化会调用 this._init(options) , 它被定义在 src/core/instance/init.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 {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    
     // 省略
}    

组件配置合并最终会调用 initInternalComponent 方法,它被定义在 src/core/instance/init.js

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // vm.constructor 就是子组件构造函数 Sub
  // 实际等于 vm.$options = Object.create(Sub.options)
  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 // 子组件父 Vue 实例
  opts._parentVnode = parentVnode // 子组件父 VNode 实例

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

initInternalComponent 执行完后,options 合并后的配置大致如下:

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>'
  }
}

参考

Vue.js 技术揭秘

Vue 源码解析系列

  1. Vue源码解析之 源码调试
  2. Vue源码解析之 编译
  3. Vue源码解析之 数据驱动
  4. Vue源码解析之 组件化
  5. Vue源码解析之 合并配置
  6. Vue源码解析之 生命周期
  7. Vue源码解析之 响应式对象
  8. Vue源码解析之 依赖收集
  9. Vue源码解析之 派发更新
  10. Vue源码解析之 nextTick