Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue的合并配置过程 #18

Open
daodaolee opened this issue Mar 4, 2021 · 0 comments
Open

Vue的合并配置过程 #18

daodaolee opened this issue Mar 4, 2021 · 0 comments

Comments

@daodaolee
Copy link
Owner

daodaolee commented Mar 4, 2021

合并配置(mergeOptions)在两个地方出现,一个是代码主动调用new Vue的时候,一个是创建子组件调用 new Vue的时候,它们都会执行 _init(options) 方法,:

来看下 _init 的逻辑:

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, 另一个用 mergeOptions,先来看下第二种:

调用了 resolveConstructorOptions(vm.constructor), 方法:

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  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
}

这里传入的Ctor参数是Vue,所以它没有 super,最后返回了 Vue 的 options。

所以在调用 mergeOptions 的时候,传入的第一个参数就是大 Vue 的 options,第二个 options 就是在代码中写 new Vue({}) 的时候传入的参数(比如render,el等),它们两个通过 mergeOptions 合并到了一起,赋值给 vm.$options ,来看下这个 mergeOptions 的逻辑,它在 src/core/util/options.js

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
}

它其实就是把参数 parent 和参数 child 合并,先递归把 extendsmixins 合并到 parent 上,然后遍历 parent,调用 mergeField ,然后再遍历 child,如果 key 不在 parent 上,就调用 mergeField

mergeField 中就是调用 strats 方法,根据传入的 key 的不同得到不同的 strat,如果没有就是 defaultStrat

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

可见 defaultStrat 优先是 parent,然后是 child (一个简单的合并策略)。接着上面的 strats,它其实就是定义了很多合并策略,strats 最开始的定义是:

const strats = config.optionMergeStrategies

optionMergeStrategies 的定义是一个空对象: Object.create(null),也就是说这个 strats 主要是用来扩展的,后面接着在 strats 上扩展了很多属性,比如data:

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

又比如 componentfilter

export const 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) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

这里主要说下生命周期是如何合并的:

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

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

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

可以看到 parentValchildVal 可以传 Function 或者 Array<Function>,返回了 Array<Function>,也就是返回了一个 Function 类型的数组,返回值逻辑:

if(子有) {
  if(父有){
    return 合并父子
  }else{
    if(子是数组){
      return 
    }else{
      return []
    }
  }
}else{
  return 
}

知道了 mergeOptions 的逻辑之后,再看下它的第一个参数:resolveConstructorOptions,这个返回 Vue 的 options,这个 Vue 的 options 定义在 src/core/global-api/index.js:

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)

initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)

// ASSET_TYPES 定义在constance.js里
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

在 Vue 初始化的时候定义了一个空对象 optionsASSET_TYPES 也都扩展到这个 options 里面,接着用 builtInComponents 扩展了一些内置组件(比如transition,keepAlive),把它们都添加到 _init 中的 vm.$options 上。

而在mixin模块:

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

这是个合并全局options的过程,同样的把全局传入的mixin对象,通过 mergeOptions 把传入的对象,也混入到 Vue 的 options 上,这些就是在执行 new Vue 的时候进行的合并。

另一个合并在子组件在初始化的时候,先回忆一下组件的构造函数过程:

/**
 * 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 对应的就是前面定义的组件对象,它会和 Vue.options 合并到 Sub.opitons 中。

接着回忆一下子组件的初始化过程:

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

vnode.componentOptions.Ctor 指向的是 Vue.extend 的返回值 Sub,所以在执行它的时候,会接着执行子组件的 this._init(options)

此时合并过程走到了 initInternalComponent 方法:

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

注意调用它的时候,传入的 vm 是子组件实例,所以里面的 vm.constructor.options 就是子组件构造器上的 options,子组件构造器是在 Vue.extend 的时候拿到的 options,所以这里的 vm.constructor 就是子组件构造函数的 Sub,相当于 vm.$options = Object.create(Sub.options),而这里用了 Object.create 方式创建,所以相当于:vm.$options.__proto__ = 子组件实例合并的options。

接着又把实例化子组件传入的子组件父 VNode 实例 parentVnode、子组件的父 Vue 实例 parent 保存到 vm.$options 中,另外还保留了 parentVnode 配置中的如 propsData 等其它的属性。

举个例子把上面的过程捋一遍(重点看多个created是如何合并的):

import Vue from 'vue'

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

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

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

new Vue 之前,会先执行 Vue.mixin,也就是合并全局的 options,也就是 Vue.options,所以第一次走到 mergeOptions 的时候,参数 parent 上没有的,参数 child 上是 created,接着进行 mergeHook 的时候,参数 parenVal 是没有的(因为new Vue({})上没有created),参数 childVal 就是 Vue.mixin 上的 created ,然后返回一个 [created(){}],此时执行 mergeFieldoptions 上多了一个 created 属性,值是 [created(){}],到此 Vue.mixin 的合并结束。

接着执行 new Vue 逻辑,在执行 _init 的时候,会执行:

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

此时 resolveConstructorOptions(vm.constructor) 返回的就是大 Vue 的 options,接着执行到 mergeOptions 的时候,parent 就是大 Vue 的 options,此时的 parent 多了一个 created,这个 created 是刚才 Vue.mixin 添加进去的,而此时的 child 就是 #app 了,这一步到最后会赋值给 vm.$options

再往后,又会继续走 mergeOptions,这次是 Vue.extend 创建子组件构造器的时候执行的:

Sub.options = mergeOptions(
  Super.options,
  extendOptions
)

因为子组件构造器是继承大 Vue 的,所以这里的 Super.options 就是大 Vue 的 options,把它和子组件自定义的对象(例子中的childComp),也就是对子组件定义的配置进行合并,所以在执行到这一步的 mergeOptions 的时候,该方法的参数 parent 就是前面合并之后的大 Vue 的 options,而参数 child 就是子组件定义的配置(例子中的childComp),注意:参数 child 上有个 created,而参数 parent 上也有 created,接着在执行到 mergeHook 的时候,就会执行:

if(子有) {
  if(父有){
    return 合并父子
  }else{
    if(子是数组){
      return 
    }else{
      return []
    }
  }
}else{
  return 
}

此时会走到合并父子,也就是代码中的 parentVak.concat(childVal),也就是 先父后子

然后 mergeField 返回的就是通过 mergeOptons 合并之后的 options,也就是子组件构造器的options,也就是 Sub.options,此时里面的 created 属性就是 [created(){}, created(){}]

到这里 new Vue 的合并就结束了,后面在执行子组件的初始化的时候,会执行 this._init

const Sub = function VueComponent (options) {
  this._init(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

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

这里的 vm.constructor.options 就是刚才合并了的 options,然后把它通过 Object.create 创建,并赋值给 vm.$options,这样的话 vm.$options 就会有一个 __proto__ 属性,它的值就是 options 的内容。

接着把 子组件的父vnode(options._parentVnode) 和 子组件的父Vue实例(options.parent),都赋值到 vm.$options 上,然后把组件创建时候的一些配置也赋值给 vm.$options,最终合并出来的 vm.$options 就有了:

vm.$options = {
  parent: Vue /*父Vue实例, options.parent*/,
  propsData: undefined,
  _componentTag: undefined,
  _parentVnode: VNode /*父VNode实例, options._parentVnode*/,
  _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: '123'
       }
    },
    template: '<div>{{msg}}</div>'
  }
}

总结

对于 new Vue 是通过 mergeOption 合并的,对于组件是通过 initInternalComponent 合并的,而 initInternalComponent 合并比较简单,所以它的合并更快。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant