「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
在前文中,即 浅羲Vue源码-4-new Vue()哪些事儿(1) 中:
-
我们分析了
Vue构造函数的由来,它是在src/core/instance/index.js中声明; -
Vue构造函数中只有一行核心代码:this._init() -
this._init()是在initMixin()方法向Vue.prototype上扩展的方法; -
_init()的细节逻辑:- 4.1
vm是Vue的实例,在后面出现vm就要想到Vue的实例; - 4.2 根据
options._isComponent处理选项的合并,根实例获得vm.$options属性; - 4.3 代理
_renderProxy到vm; - 4.4 一系列的初始化和
beforeCreated和created钩子的调用; - 4.5 最后根据
vm.$options.el属性决定是否调用vm.$mount方法实施挂载;
- 4.1
本篇小作文将继续围绕 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
)
}
从图上可以很清晰的看到,当执行到
new Vue 的时候,vm._isComponent 为 undefined,所以会走 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 中都包含了些什么:
这个对象就是前面一直在说的 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
方法作用:
- 接收父选项
parent,子选项child,和vm实例,合并父子选项,并且会标准化处理props、inject、directives,方便后面的进一步处理这些选项; - 当
child没有_base属性时,说明不是Vue.options,同时child有mixin或extends属性时,将extends和mixin合并到父选项parent; - 合并选项时,如遇相同属性,则采用子选项覆盖父选项的策略合并,当然也可以指定策略。最终返回合并后的选项
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 属性,看下合并后的结果如下图:
2.3 合并后 Vue.options 中的原来的components/directives/filters 去哪了?
从上面 2.2 最后的图不难看出,合并完的结果 vm.$options 中的 components 中只有 someCom 这个子组件(test.html 中 new Vue 时创建的子组件),那么那些内建的全局组件或者执行令,比如 KeepAlive、model/show 都去哪里了。
在前面的几篇文章中,一直提到 Vue 的 mergeOpitons 会是 Vue 实现代码复用或者说全局组件或全局指令的实现原理,玄机就在这里了。在说着答案之前我们在来看张图:
各位看官老爷都是前端泰斗,这很明显,全局的组件如 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.components 即 KeepAive 为啥去了 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 的全局组件,directives 和 filters 同理,这里不再赘述;
三、总结
本文详细讲述了,new Vue(options) 时,执行 this._init 方法中判断 options._isComponent 为 undefined 时执行 vm.$options = mergeOptions(....) 的逻辑;
- 用
resolveConstructorOptions获取Vue.options选项对象,这个东东是initGlobalAPI创建的; mergeOptions接收父选项(Vue.options) 和子选项(new Vue传递的选项),进行合并,期间标准化处理了inject和props等;- mergeOptions 合并
components、directives、filters是有策略的,这个策略会让当前实例继承全局的components、directives、filters