浅曦Vue源码-5-new Vue()那些事儿(2)-mergeOptions

881 阅读5分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

在前文中,即 浅羲Vue源码-4-new Vue()哪些事儿(1) 中:

  1. 我们分析了 Vue 构造函数的由来,它是在 src/core/instance/index.js 中声明;

  2. Vue 构造函数中只有一行核心代码:this._init()

  3. this._init() 是在 initMixin() 方法向 Vue.prototype 上扩展的方法;

  4. _init() 的细节逻辑:

    • 4.1 vmVue 的实例,在后面出现 vm 就要想到 Vue 的实例;
    • 4.2 根据 options._isComponent 处理选项的合并,根实例获得 vm.$options 属性;
    • 4.3 代理 _renderProxyvm
    • 4.4 一系列的初始化和 beforeCreatedcreated 钩子的调用;
    • 4.5 最后根据 vm.$options.el 属性决定是否调用 vm.$mount 方法实施挂载;

本篇小作文将继续围绕 new Vue 的核心 _init 方法展开 mergeOptions 细节,即获取 vm.$options 的相关细节,结合断点调试理解每一步都做了什么,在阅读中,注意留心 Vue.options 对象。

二、_init 中的选项合并

组件合并时,会根据 options._isComponent 选择不同的合并方式,如果是组件则执行 initInternalComponent 方法,否则执行 mergeOptions 并将返回值作为 vm.$options 的值:

if (options && options._isComponent) {
  initInternalComponent(vm, options)
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor), // vm.constructor 就是 Vue 啊。。。。
    options || {},
    vm
  )
}

image.png 从图上可以很清晰的看到,当执行到 new Vue 的时候,vm._isComponentundefined,所以会走 else,执行 mergeOptions 方法:

2.1 resolveContructorOptions 方法

声明位置为:src/core/instance/init.js -> resolveConstructorOptions

在说 mergeOptions 之前先说 resolveConstructorOptions 方法,因为 mergeOptions 方法接收了 resolveConstructorOptions 方法的返回值;

该方法的作用是从组件的构造函数中解析构造函数的 options 属性,然后根据构造函数 Ctor 是否有 super 这个父类这个属性做出不同操作,Ctor.super 不存在的时候,直接返回 options,此时 Ctor 就是 Vue 自身,说明当前正在创建根实例,我们 new Vue 执行 resolveConstructorOptions 时就是这个逻辑;

注意这里这个构造函数在当前 new Vue 的时候指的是 Vue 自身,但是当后面创建组件时,比如我们的 test.html 中的子组件 someCom ,该子组件的构造函数其实是 Vue 的一个子类;

export function resolveConstructorOptions (Ctor: Class<Component>) {
  // Ctor.options 这里面有一些 directives: { model, show }, component: { keepAlive, Transition, } filter: {}
  // 我很好奇,这些玩意儿是什么时候添加的?
  // 是 core/index.js 中调用 intGlobalApi 的时候添加的,通过 extend(Vue.options, 'xxxx', builtInxxx)
  let options = Ctor.options
  
  // 所以进入到 if 条件中的语句都是创建组件,此时是根实例创建,暂时忽略其中逻辑
  if (Ctor.super) {
  }
  return options
}

我们来看看 Vue.options 中都包含了些什么:

image.png

这个对象就是前面一直在说的 initGlobalAPI 的时候创建的,其中 components/directives/filter 分别对应了:全局组件、全局指令、全局过滤器,全局组件中的 KeepAlive 就是我们使用的 <keep-alive />directives 中的 model 就是 v-model,而 _base 就是 Vue 构造函数自身。

综上,在调用 mergeOptions 前调用 resolveConstructorOptions 时得到的就是 Vue.otpions 这个对象。

2.2 mergeOptions

方法声明位置:src/core/util/options.js

方法作用:

  1. 接收父选项 parent,子选项 child,和 vm 实例,合并父子选项,并且会标准化处理 props、inject、directives,方便后面的进一步处理这些选项;
  2. child 没有 _base 属性时,说明不是 Vue.options,同时 childmixinextends 属性时,将 extendsmixin 合并到父选项 parent
  3. 合并选项时,如遇相同属性,则采用子选项覆盖父选项的策略合并,当然也可以指定策略。最终返回合并后的选项
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (typeof child === 'function') {
    child = child.options
  }

  // 标准化 props、inject、directive 选项,以便进一步处理
  // normalizeInject 就会把 inject 处理成 { from: key, default: xxx } 这种标准处理形式,后面 initInject 方法会用到这一数据结构
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // 处理 child 对象上的 extends 和 mixins,将这些继承而来的选项合并到 parent
  // mergeOptions 处理过的对象含有 _base 属性
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  
  // 遍历父选项,合并选项,期间会处理相同选项,下面 mergeField 方法中有策略
  let key
  // 遍历父 options
  for (key in parent) {
    mergeField(key)
  }

  // 遍历子选项,把父选项中没有的选项合并到 options,
  // 而因为父子相同的属性时在上面处理父选项时已经处理了,就是前面的 for in 循环
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }

  // 合并字段,child 选项将覆盖子选项
  function mergeField (key) {
    // strats 或者 defaultStrat 是个合并策略,即到底用父的还是用子的
    const strat = strats[key] || defaultStrat
    // 优先使用 child 子选项的值
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

总结起来讲,new Vue 执行到 mergeOptions 时,先调用 resolveConstructorOptions 获取 Vue.options 这个有全局组件/指令/过滤器 的选项对象,然后传递给 mergeOptions 作为父选项,和 new Vue(子选项) 时传递的子选项进行合并,合并的最终结果赋值到 vm.$options 属性,看下合并后的结果如下图:

image.png

2.3 合并后 Vue.options 中的原来的components/directives/filters 去哪了?

从上面 2.2 最后的图不难看出,合并完的结果 vm.$options 中的 components 中只有 someCom 这个子组件(test.htmlnew Vue 时创建的子组件),那么那些内建的全局组件或者执行令,比如 KeepAlive、model/show 都去哪里了。

在前面的几篇文章中,一直提到 VuemergeOpitons 会是 Vue 实现代码复用或者说全局组件或全局指令的实现原理,玄机就在这里了。在说着答案之前我们在来看张图:

image.png

各位看官老爷都是前端泰斗,这很明显,全局的组件如 KeepAlive 都去到 vm.$options.components.__proto__ 属性了,看了这,我感觉我又行了,这不明显就是继承么?

从这里不难看出,vm 是通过原型链的查找机制找到这些全局的组件或者指令的,这就是我们一直说的全局组件的实现。他实现不是简单粗暴的复制到每个子实例,而是通过继承实现的。接下来我们看看这是谁干的好事:

2.2 mergeOptions 中,最底下有个方法叫做 mergeFiled,这个问题将由它来揭晓答案:

2.4 mergeFile 策略合并实现全局组件的继承

mergeFiled 本身不复杂,将父子属性传给策略,交给策略去合并,策略即 strats 对象,starts 是个常量,这个常量中各种策略的合并是下面的代码进行的,来自以下代码:

const strats = config.optionMergeStrategies;

// ASSET_TYPES = [
//   'component',
//   'directive',
//   'filter'
// ]
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

从上面可以看得出来,组件 components/directives/filters 的合并策略都指向了一个 mergeAssets 的方法:

方法位置:src/core/util/options.js -> mergeAssets

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  // Object.create 不就是创建以 parentVal 为原型的对象
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

相信此时到这里,你已经明白为啥 Vue.options.componentsKeepAive 为啥去了 vm.$options.components 的 __proto__ 上了吧?因为 mergeFile(Vue.options['components'], vm.$options['compoennts'])

紧接着,在 mergeField 调用策略,即 mergeAsset(Vue.options['components'], vm.$options['compoennts']),把这两个对象带入到 mergeAssets 中试试吧。

这就得到了 res = Object.create(Vue.options['components']); 此时 res.__proto__ 就是 Vue.options['components'],即有 KeepAlive 的全局组件,directivesfilters 同理,这里不再赘述;

三、总结

本文详细讲述了,new Vue(options) 时,执行 this._init 方法中判断 options._isComponentundefined 时执行 vm.$options = mergeOptions(....) 的逻辑;

  1. resolveConstructorOptions 获取 Vue.options 选项对象,这个东东是 initGlobalAPI 创建的;
  2. mergeOptions 接收父选项(Vue.options) 和子选项(new Vue 传递的选项),进行合并,期间标准化处理了 injectprops 等;
  3. mergeOptions 合并 components、directives、filters 是有策略的,这个策略会让当前实例继承全局的 components、directives、filters