重学Vue【Vue的合并配置过程】

简介: 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。


正文


合并配置(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,返回了 Array,也就是返回了一个 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 合并比较简单,所以它的合并更快。

目录
相关文章
|
5天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
vue学习第四章
|
5天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
vue学习第九章(v-model)
|
5天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
vue学习第十章(组件开发)
|
10天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
11天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
11天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
11天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
10天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
10天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
JavaScript