• Vue2源码学习笔记 - 5.options选项合并


    上一节我们学习了 Vue 应用实例的初始化过程,其中有很多细节仍需我们去研读,这一节我们就先来研究分析 options 选项的合并过程。

    我们继续回到 _init 的代码段,它在文件 /src/core/instance/init.js 中,这个方法在应用和组件实例化时都是必须调用的,options 选项合并就在这里被执行。

    // Vue.prototype._init 方法代码段
    ...
    // merge options
    if (options && options._isComponent) {
      // 组件实例化时合并选项
      initInternalComponent(vm, options)
    } else {
        // Vue 应用实例化时合并选项
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    选项合并在两个情况下都会发生,从代码分支语句看,一个是常见的 new Vue(options) 的时候,另一个情况就是实例化组件的时候。我们先看 new Vue 实例化时的合并过程,它先调用 resolveConstructorOptions(vm.constructor),相当于直接调用了 resolveConstructorOptions(Vue),这个函数在这个情况下是直接返回 Vue.options 的。

    那么,有其他情况吗?有,那就是在创建组件的 Vnode 节点的时候,调用它重新合并组件选项,这个我们后面说。继续先看看 Vue.options,它先在 /src/core/global-api/index.js 中定义

    // /src/core/global-api/index.js
    ...
    Vue.options = Object.create(null)
    // ASSET_TYPES 为常量数组
    // const ASSET_TYPES = ['component','directive','filter'] <-----
    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
    // builtInComponents = KeepAlive 组件
    extend(Vue.options.components, builtInComponents)
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    设置 Vue.options 空对象,然后遍历 ASSET_TYPES 添加属性 component,directive,filter,然后把内置组件 KeepAlive 合并到 Vue.options.components 上。之后在 /src/platforms/web/runtime/index.js 中合并平台相关的组件

    ...
    import platformDirectives from './directives/index'
    import platformComponents from './components/index'
    ...
    // install platform runtime directives & components
    // 合并 平台相关 的指令(v-model,v-show)
    extend(Vue.options.directives, platformDirectives)
    // 合并 平台相关 的组件(Transition, TransitionGroup)
    extend(Vue.options.components, platformComponents)
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这里导入的 platformDirectives 为 web 平台下的指令,如:v-show,v-model;platformComponents 为 web 平台下的内置组件,如:Transition, TransitionGroup。最后,在使用 Vue.component 和 Vue.directive 注册组件和指令时也会分别写入 Vue.options.components 和 Vue.options.directives 对象里。那么最终,Vue.options 大概就是这样:

    Vue.options = {
      components: {
        Blog: ƒ VueComponent(options), // 自定义组件
        Hello: ƒ VueComponent(options),// 自定义组件
        Test: ƒ VueComponent(options), // 自定义组件
        // Vue 内置组件
        KeepAlive: {name: 'keep-alive', abstract: true, props: {}, methods: {}, created: ƒ,},
        Transition: {name: 'transition', props: {}, abstract: true, render: ƒ},
        TransitionGroup: {props: {}, methods: {}, beforeMount: ƒ, render: ƒ, updated: ƒ}
      },
      directives: {
          // 内置指令
        model: {inserted: ƒ, componentUpdated: ƒ}, // v-model
        show: {bind: ƒ, update: ƒ, unbind: ƒ} // v-show
      },
      filters: {},
      _base: ƒ Vue(options) // Vue.options._base === Vue
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这个 Vue.options 和 实例化传入的 options 再传给 mergeOptions,它的定义在文件 /src/core/util/options.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') {
        // 遍历检测 options.components 的组件名
        checkComponents(child)
      }
    
      if (typeof child === 'function') {
        child = child.options
      }
      // 过滤并格式化 options.props 属性为 对象格式保存
      normalizeProps(child, vm)
      // 过滤并格式化 options.inject 属性为 对象格式保存
      normalizeInject(child, vm)
      // 格式化纯函数指令 options.directives 为 对象格式保存
      normalizeDirectives(child)
    
      // Apply extends and mixins on the child options,
      // but only if it is a raw options object that isn't
      // the result of another mergeOptions call.
      // Only merged options has the _base property.
      if (!child._base) {
        if (child.extends) {
          // 扩展属性 options.extends 与父 options 合并
          parent = mergeOptions(parent, child.extends, vm)
        }
        if (child.mixins) {
          // 混入属性 options.mixins 与父 options 合并
          for (let i = 0, l = child.mixins.length; i < l; i++) {
            parent = mergeOptions(parent, child.mixins[i], vm)
          }
        }
      }
    
      const options = {}
      let key
      // 根据 parent 中的 key 调用 mergeField 合并选项
      for (key in parent) {
        mergeField(key)
      }
      // 根据在 child 中且不在 parent 中的 key 继续 调用 mergeField 合并选项
      for (key in child) {
        if (!hasOwn(parent, key)) {
          mergeField(key)
        }
      }
      // 从 strats 中获取具体的选项合并函数执行合并操作
      function mergeField (key) {
        const strat = strats[key] || defaultStrat
        options[key] = strat(parent[key], child[key], vm, key)
      }
      return options
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    在 mergeOptions 中先是检查 options.components 里的组件名,后面过滤处理 props,inject,directives,然后合并 extends 和 mixins。最后先遍历 parent 选项,再遍历 child 选项,调用 mergeField 并把 key 传入,进行合并操作。来看看函数 mergeField 中 strats 的定义:

    const strats = config.optionMergeStrategies
    
    if (process.env.NODE_ENV !== 'production') {
      strats.el = strats.propsData = function (parent, child, vm, key) {
        ...
      }
    }
    
    export function mergeDataOrFn (
      parentVal: any,
      childVal: any,
      vm?: Component
    ): ?Function {
      if (!vm) {
        // in a Vue.extend merge, both should be functions
        if (!childVal) {
          return parentVal
        }
        if (!parentVal) {
          return childVal
        }
        // when parentVal & childVal are both present,
        // we need to return a function that returns the
        // merged result of both functions... no need to
        // check if parentVal is a function here because
        // it has to be a function to pass previous merges.
        return function mergedDataFn () {
          return mergeData(
            typeof childVal === 'function' ? childVal.call(this, this) : childVal,
            typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
          )
        }
      } else {
        return function mergedInstanceDataFn () {
          // instance merge
          const instanceData = typeof childVal === 'function'
            ? childVal.call(vm, vm)
            : childVal
          const defaultData = typeof parentVal === 'function'
            ? parentVal.call(vm, vm)
            : parentVal
          if (instanceData) {
            return mergeData(instanceData, defaultData)
          } else {
            return defaultData
          }
        }
      }
    }
    
    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)
    }
    
    function mergeHook (
      parentVal: ?Array<Function>,
      childVal: ?Function | ?Array<Function>
    ): ?Array<Function> {
      const res = childVal
        ? parentVal
          ? parentVal.concat(childVal)
          : Array.isArray(childVal)
            ? childVal
            : [childVal]
        : parentVal
      return res
        ? dedupeHooks(res)
        : res
    }
    
    LIFECYCLE_HOOKS.forEach(hook => {
      strats[hook] = mergeHook
    })
    
    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
      }
    }
    
    ASSET_TYPES.forEach(function (type) {
      strats[type + 's'] = mergeAssets
    })
    
    strats.watch = function (
      parentVal: ?Object,
      childVal: ?Object,
      vm?: Component,
      key: string
    ): ?Object {
      // work around Firefox's Object.prototype.watch...
      if (parentVal === nativeWatch) parentVal = undefined
      if (childVal === nativeWatch) childVal = undefined
      /* istanbul ignore if */
      if (!childVal) return Object.create(parentVal || null)
      if (process.env.NODE_ENV !== 'production') {
        assertObjectType(key, childVal, vm)
      }
      if (!parentVal) return childVal
      const ret = {}
      extend(ret, parentVal)
      for (const key in childVal) {
        let parent = ret[key]
        const child = childVal[key]
        if (parent && !Array.isArray(parent)) {
          parent = [parent]
        }
        ret[key] = parent
          ? parent.concat(child)
          : Array.isArray(child) ? child : [child]
      }
      return ret
    }
    
    strats.props =
    strats.methods =
    strats.inject =
    strats.computed = function (
      parentVal: ?Object,
      childVal: ?Object,
      vm?: Component,
      key: string
    ): ?Object {
      if (childVal && process.env.NODE_ENV !== 'production') {
        assertObjectType(key, childVal, vm)
      }
      if (!parentVal) return childVal
      const ret = Object.create(null)
      extend(ret, parentVal)
      if (childVal) extend(ret, childVal)
      return ret
    }
    strats.provide = mergeDataOrFn
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160

    那么 strats 最终看起来应该就是这样

    strats = {
      activated: ƒ mergeHook( parentVal, childVal ),
      beforeCreate: ƒ mergeHook( parentVal, childVal ),
      beforeDestroy: ƒ mergeHook( parentVal, childVal ),
      beforeMount: ƒ mergeHook( parentVal, childVal ),
      beforeUpdate: ƒ mergeHook( parentVal, childVal ),
      components: ƒ mergeAssets( parentVal, childVal, vm, key ),
      computed: ƒ ( parentVal, childVal, vm, key ),
      created: ƒ mergeHook( parentVal, childVal ),
      data: ƒ ( parentVal, childVal, vm ),
      deactivated: ƒ mergeHook( parentVal, childVal ),
      destroyed: ƒ mergeHook( parentVal, childVal ),
      directives: ƒ mergeAssets( parentVal, childVal, vm, key ),
      el: ƒ (parent, child, vm, key),
      errorCaptured: ƒ mergeHook( parentVal, childVal ),
      filters: ƒ mergeAssets( parentVal, childVal, vm, key ),
      inject: ƒ ( parentVal, childVal, vm, key ),
      methods: ƒ ( parentVal, childVal, vm, key ),
      mounted: ƒ mergeHook( parentVal, childVal ),
      props: ƒ ( parentVal, childVal, vm, key ),
      propsData: ƒ (parent, child, vm, key),
      provide: ƒ mergeDataOrFn( parentVal, childVal, vm ),
      serverPrefetch: ƒ mergeHook( parentVal, childVal ),
      updated: ƒ mergeHook( parentVal, childVal ),
      watch: ƒ ( parentVal, childVal, vm, key )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    那么在 mergeField 中对于每个 options 的 key 调用 strats 中对应的合并函数来执行合并操作,比如 filters 选项调用 mergeAssets 合并,created 等生命钩子选项调用 mergeHook 合并,还有 data\el\props\computed 等也调用相应的函数合并。
    Vue选项合并流程

    总结:

    这里我们主要详细学习 new Vue 应用实例化的选项合并策略,简而言之就是合并 Vue.options 与 new Vue(options) 中的 options 为新的选项对象并保存在实例 vm 的 $options 属性上。合并的具体策略是函数 mergeField 调用 strats 中定义的合并函数并返回,如上图所示。组件的选项合并分了两个阶段,我们在学习组件的相关知识点时在详细介绍。

  • 相关阅读:
    Java—异常
    四阶龙格库塔与元胞自动机
    关于ip地址的网页无法访问navigator的gpu、媒体、蓝牙等设备的解决方法
    【GYM 102832H】【模板】Combination Lock(二分图博弈)
    Python实现逐步回归
    中断系统中的设备树__Linux对中断处理的框架及代码流程简述
    (持续更新中!)详解【计算机类&面试真题】军队文职考试 ——第二期(真题+解析)| 网络协议的三个核心要素;交互式系统中的非剥夺策略;中断和陷入的区别;可变分区管理中的硬件机制;数据库系统的优点
    linux环境搭建nacos集群详解
    记录关于Ajax二次加载出的内容无法再次实现Ajax点击效果问题
    python-web开发[13]之前端js
  • 原文地址:https://blog.csdn.net/dclnet/article/details/125907128