• Vue2.0源码理解(4) - 合并配置


    合并配置

    通过之前的源码学习,我们已经了解到了new Vue主要有两种场景,第一种就是在外部主动调用new Vue创建一个实例,第二个就是代码内部创建子组件的时候自行创建一个new Vue实例。但是无论那种new Vue方式,我们都需要进入了Vue._init,执行mergeOptions函数合并配置。为了更直观,我们整个demo调试耍耍。

    // src\main.js
    let childComp = {
      template:"<div>{{msg}}</div>",
      data(){
        return{
          msg:"childComp"
        }
      },
      created(){
        console.log("childComp created");
      },
      mounted(){
        console.log("childComp mounted");
      }
    }
    
    Vue.mixin({
      created(){
        console.log("mixin");
      }
    })
    
    let app = new Vue({
      el:"#app",
      render: h => h(childComp)
    })
    

    我用的时vue-cli3,这里有个小细节需要注意一下,vue-cli3开发环境默认使用的是runtime版本(node_modules\vue\dist\vue.runtime.esm.js),这个版本是不支持编译template的,需要用Compiler版本,这个在vue.config.js中配置一下即可,配置代码如下:

    module.exports = {
        runtimeCompiler: true
    }
    

    准备工作搞好了,那么我们现在开始进入_init函数,看看合并配置是怎么一个说法。

    // src\core\instance\init.js
    Vue.prototype._init = function (options?: Object) {
        ...
        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),  //vue.options
            options || {},  //new Vue中的options
            vm
          )
        }
        ...
    }
    

    外部调用场景

    上述代码中可明显看出两中合并配置的情况,我们一开始进入的肯定时非组件模式,也就是else情况。mergeOptions传入了3个入参,我们先看第一个入参的resolveConstructorOptions方法做了什么。

    // src\core\instance\init.js
    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 = vm.constructor = Vue,Vue没有父级,所以不会进入到if逻辑,因此这里返回的就是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)
    })
    Vue.options._base = Vue     //createComponent时用到,之前提及过。
    extend(Vue.options.components, builtInComponents)   //扩展一些内置组件
    

    这里ASSET_TYPES在src\shared\constants.js有定义

    // src\shared\constants.js
    export const ASSET_TYPES = [
      'component',
      'directive',
      'filter'
    ]
    

    然后我们再返回去_init函数分析一下mergeOptions函数:

    // src\core\util\options.js
    export function mergeOptions (
      parent: Object,
      child: Object,
      vm?: Component
    ): Object {
      ...  
      const options = {}
      let key
      、、
      for (key in parent) {
        mergeField(key)
      }
      for (key in child) {
        //   key没在parent定义时
        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(Vue.options)和child(new Vue中的options),然后遍历的过程中调用了mergeField方法。而该方法先去拿到一个strat函数,这个函数首先是再strats中去找,没找到就使用defaultStrat默认函数(defaultStrat可自行查阅源码),我们主要看strats:

    // src\core\util\options.js
    const strats = config.optionMergeStrategies
    

    strats是定义在config中,所以说我们是可以随意改动strats的。然后在options.js中,strats扩展了很多属性,每个属性(key)都是一种合并策略,有兴趣的可以一个个研究,因为我们例子是生命周期的合并,所以我们先挑生命周期的合并策略来分析,后面遇到其他的再做分析。

    // src\core\util\options.js
    LIFECYCLE_HOOKS.forEach(hook => {
      strats[hook] = mergeHook
    })
    

    LIFECYCLE_HOOKS定义在src\shared\constants.js

    // src\shared\constants.js
    export const LIFECYCLE_HOOKS = [
      'beforeCreate',
      'created',
      'beforeMount',
      'mounted',
      'beforeUpdate',
      'updated',
      'beforeDestroy',
      'destroyed',
      'activated',
      'deactivated',
      'errorCaptured'
    ]
    

    遍历这些值,然后定义它们的合并策略,其实都mergeHook方法,都是一样的合并策略,下面我们看看mergeHook函数:

    // src\core\util\options.js
    function mergeHook (
      parentVal: ?Array<Function>,
      childVal: ?Function | ?Array<Function>
    ): ?Array<Function> {
      return childVal
        ? parentVal
          ? parentVal.concat(childVal)
          : Array.isArray(childVal)
            ? childVal
            : [childVal]
        : parentVal
    }
    

    这个多层嵌套的三元表达式看着复杂,其实不难,我们可以分段理解:
    ①:childVal有值:进入②,
           childVal没值:赋值parentVal;
    ②:parentVal有值:parentVal和childVal数组合并,
           parentVal没值:进入③;
    ③:childVal是个数组:赋值childVal,
           childVal不是数组:赋值[childVal];
    最终我们return了一个数组到mergeOptions函数。

    现在我们回过头来demo中的Vue.mixin定义,其源码其实也调用了mergeOptions,我们看看源码:

    // src\core\global-api\mixin.js
    export function initMixin (Vue: GlobalAPI) {
      Vue.mixin = function (mixin: Object) {
        this.options = mergeOptions(this.options, mixin)
        return this
      }
    }
    

    mixin的源码很简单,其实就是调用了mergeOptions对Vue.options做了合并。有个小细节需要留意,就是demo中Vue.mixin和new Vue的代码顺序,必须先对Vue.mixin做出定义,不然在new Vue的时候Vue.options和new Vue的options合并时,是会丢失掉Vue.mixin的,因为那时候Vue.mixin并没有执行mergeOptions把options合并到Vue.options上。

    组件场景

    接下来我们看另一种情况,组件合并配置。也就是在_inti方法中运行了initInternalComponent函数,我们来分析一下它做了什么?

    // src\core\instance\init.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
      }
    }
    

    子组件的合并就相对简单很多了,vm.$options去继承了子组件构造器vm.constructor.options,然后再把一些配置挂载到上面。我们主要看看vm.constructor.options是怎么来的。

    // src\core\global-api\extend.js
    Vue.extend = function (extendOptions: Object): Function {
        const Super = this
        ...
        const Sub = function VueComponent (options) {
          this._init(options)
        }
        // 构造器指向自己
        Sub.prototype.constructor = Sub
        // 合并配置
        Sub.options = mergeOptions(
          Super.options,
          extendOptions
        )
        ...
    }
    

    其实Vue.extend的时候对子组件的构造器进行了定义了,还对Vue.options(Super.options)和子组件的options(extendOptions)做了合并。
    所以initInternalComponent中的vm.$options其实就是一个已经把Vue.options和子组件的options合并好的配置集合了。

    总结

    至此Vue的options合并就告一段落了,我们需要知道它有两个场景,外部调用场景和组件场景。
    其实一些库、框架的设计也是类似的,都会有自身的默认配置,同时又允许在初始化的时候让开发者自定义配置,之后再合并两个配置来达到应付各种场景需求,这种设计思想也是我们写组件或做架构的时候必不可少的思维模式。

  • 相关阅读:
    局域网下共享文件夹全流程
    AmzTrends x TiDB Serverless:通过云原生改造实现全局成本降低 80%
    幼儿安全消防知识教案
    运动耳机什么牌子的好?运动耳机推荐排名
    计算机毕业设计 基于Web的视频及游戏管理平台的设计与实现 Java实战项目 附源码+文档+视频讲解
    计算机组成原理--数据表示
    安卓开发实例:方向传感器
    什么是DNS域名解析?
    有损压缩算法
    【vue.js】使用高德地图选择省市区后,再点击确认当前选择的位置
  • 原文地址:https://www.cnblogs.com/YmmY/p/15915528.html