返回
专栏第五篇-丰富的选项合并策略
一、实例化时合并选项
在上一篇文章中执行Vue初始化的时候,我们说到了选项合并。
那什么是选项呢?
在 new Vue(选项)中,Vue构造函数中传入的数据就是选项,代表传入的选项,一般以对象形式传入。
在Vue源码中使用变量options表示选项,在源码中你经常能看到 options的身影。
在实例化时需要将实例对应的构造函数上的选项和传入的选项合并到实例的选项上。
1 | vm.$options = mergeOptions( |
上面 mergeOptions 则是用于合并 2 个 options,resolveConstructorOptions用于获取构造函数上的options。
在前面的文章 Vue的原型设计中,我们知道 vm.constructor 就是指的 Vue构造函数
二、Vue构造函数的默认选项
前面我们知道 vm.$options 中会合并Vue构造函数的 options。
那么 Vue.options 有值吗?
实际上Vue构造函数本身会自带一些默认的选项。
在Vue被引入时,会执行多个方法给Vue.options注入属性。
2.1 initGlobalAPI方法创建 Vue.options
initGlobalAPI方法在Vue被引入时执行。
1 | // core/global-api/index.ts |
可以看到,installGlobalAPI方法中创建了 Vue.options为一个纯净的空对象,然后在options上面注入了一些属性。
_base就是 Vue构造函数。每个Vue组件都是通过_base属性获取到Vue构造函数,然后使用Vue.extend来生成对应的 VueComponent构造函数。extend方法是vue中的一个通用方法。
用于将第二个参数的值合并到第一个参数中,返回第一个参数。
第二个参数的值直接覆盖进第一个参数
extend函数源码
1 | export function extend( |
2.2 合并指令 & 内置组件
1 | // platforms/web/runtime/index |
该段逻辑也是在 Vue被引入时执行,扩展了一些跟 web平台相关的指令和组件。
2.3 总结
经过我们的研究发现,Vue构造函数的默认选项有:
- _base: 代表Vue构造函数,后续用来创建组件的构造函数VueComponent。
- directive:代表需要注册的指令,默认的提供了 v-model、v-show的内置指令。
- components:代表需要注册的组件选项,默认提供了 KeepAlive、Transition、TransitionGroup的内置组件。
- filter :代表需要注册的过滤器,默认没有提供默认值。
三、子类构造函数的 options
学习完上节我们知道在Vue被引入时,在Vue构造函数上注入了一些默认选项。
在专栏第三篇中,我们介绍了 Vue.extend函数。
在render阶段生成虚拟节点时,会调用 Vue.extend 生成组件构造函数VueComponent,并存在 vnode上。
然后在update阶段开始挂载节点时,会调用组件构造函数 VueComponent来创建组件实例,再进行后续组件实例初始化、渲染等操作。
那么VueComponent构造函数作为Vue构造函数的子类,是不是也继承了它的默认选项呢?
我们再次打开 Vue.extend 的源码一探究竟。
1 | Vue.extend = function(extendOptions){ |
虽然我们不知道 mergeOptions的具体逻辑,但是我们很容易看出来,VueComponent构造函数上的 options属性合并了“父类“构造函数上的options属性以及extend方法传入的选项。
在 VueComponent构造函数上新增了一个_Ctor属性,可以避免每次重新创建子类,提高性能,后面我们会专门说这里,这里不进行展开。
所以我们可以得出一个结论:子类构造函数的选项继承了其父类构造函数的选项。
举个例子:
1 | const VueComponent = Vue.extend({ |
此时 VueComponent.options 上既包含了自身传入的 template 选项,也包含了继承自Vue构造函数上的属性。
这里需要注意的是,在合并父类构造函数的 options时,不同 option的合并策略不同,对于components、filter、directives等内置选项会合并到原型链中。
我们知道,VueComponent构造函数本身是具有再次扩展的能力的。
1 | const VueComponentChild = VueComponent.extend({ |
同理,VueComponentChildConstructor是VueComponent的子类,所以VueComponentChildConstructor就继承了VueComponent的 options。
四、获取构造函数上的 options
在Vue初始化操作执行时,使用 resolveConstructorOptions 函数来获取构造函数上的 options。
1 | export function resolveConstructorOptions(Ctor){ |
看到这么多代码,想必大家也是极其懵逼的。
因为我们之前说过:
- Vue构造函数的选项是在初始化时注入的。
- VueComponent构造函数及其扩展子类的选项则是在extend方法中进行注入的。
那直接使用.options获取构造函数的选项不就行了,为什么还有这么一大段逻辑呢?
1 | Vue.options // 获取Vue构造函数的选项 |
其实这一段逻辑主要是应对父类构造函数上选项变化的情况。
4.1 Ctor.super
可以看到只有 Ctor.super 存在时才会走这一大段逻辑。
那么 Ctor.super 指的是什么呢?
在使用 extend生成子类构造函数时,会在子类构造函数上新增了一个 super属性,指向它的父类。
1 | Vue.extend = function(){ |
所以如果存在 super 属性,则代表现在这里的 Ctor使用的是extend生成的VueComponent构造函数。所以这里分为 2 种情况:
没有super属性,代表这是 Vue构造函数,直接返回 Vue.options 即可。
如果存在super属性,代表这是extend生成的 VueComponent构造函数,需要进行进一步判断。
因为 VueComponent.options的值是在 extend时合并了父类选项和extend传入选项的全新选项。
所以如果后续Vue.options变化了无法获取最新的选项。
4.2 判断父类构造函数上的 options 是否变化
那么有哪些操作可以修改父类构造函数上的 options呢?
比如使用 Vue.mixin api,这个 api就修改了Vue.options选项。
这个时候就需要有一些逻辑可以更新 VueComponent上的 options。
我们来看下 Vue是怎么更新的。
1 | const superOptions = resolveConstructorOptions(Ctor.super) |
上面代码中如果 superOptions 不等于 cachedSuperOptions,即表示父类构造函数发生了变化。
所以我们需要搞明白这 2 个值分别表示什么?
- superOptions是在resolveConstructorOptions中递归向上查找的,就是表示父类构造函数的最新选项。
- cachedSuperOptions 是指的构造函数上的 superOptions属性,这个属性是在 extend中定义的:
1 | Vue.extend = function(){ |
所以这 2 个值都是指的父类构造函数,指向的是同一块内存地址,那么为什么会有不一样的情况呢?
在 Vue 旧版本中曾经有一个相关的 bug。我们先来了解一下这个 bug:
4.2.1 Vue旧版本的 bug
这个bug的大概意思就是说:先生成VueComponent构造函数,然后再在构造函数上的 options 添加属性,在resolveComponentOptions函数执行后,后添加的属性消失了。
这是复现链接:options消失。
我们这里看一下代码:
1 | const Test = Vue.extend({ |
可以看到首先使用 Vue.extend 生成了一个 Test构造函数。
然后在 Test的 options上新增了 2 个属性。
执行完Vue.mixin后,先前定义的computed、beforeCreate2个属性不见了。
4.2.2 Vue.mixin
那么这个 Vue.mixin究竟是干了啥呢?
1 | Vue.mixin = function (mixin: Object) { |
可以看到mixin函数仅仅是改变了构造函数上的options。
但是 mergeOptions 会返回一个新的对象,导致构造函数的 options 发生了变化。
也就导致了前面说的 superOptions !== cachedSuperOptions 情况的发生。
因为 superOptions 获取的是当前最新的选项,也就是 mixin 执行过的合并选项。
而 cachedSuperOptions 则是在执行 Vue.extend 时当时的父类构造函数的选项。
4.2.3 旧版本Vue中 resolveConstructorOptions 的逻辑是什么?
但是 Vue.mixin 仅仅是更改了 Vue.options。
应该不会将VueComponent构造函数自身添加的属性清除。
所以应该是 Vue内部对其做了一些特殊处理。
我们打开 Vue 2.1.10 版本的相关源码。
1 | export function resolveConstructorOptions (Ctor) { |
- 获取了父构造函数的当前的 options:const superOptions = Ctor.super.options。
- 获取了父构造函数执行extend时的options:const cachedSuperOptions = Ctor.superOptions。
- 获取了子构造函数 extend时传入的选项extendOptions。
- 因为执行了mixin,导致父构造函数中的 options发生了变化,即superOptions !== cachedSuperOptions,然后继续执行内部的逻辑。
1 | options = Ctor.options = mergeOptions(superOptions, extendOptions) |
可以看到它是将获取到的父构造函数 options和当初 extend传入的 options合并,然后重新赋值给了 Ctor.options。
所以后添加的computed
、beforeCreate
就消失了,因为指向了不同的内存空间。
4.2.4 总结
通过上面几节的学习,我们知道了为什么需要判断父类构造函数的变化。
我们系统提供了全局注入的 API:Vue.mixin。
使用这个函数可以向全局注入一些选项。
而实际上就是通过改变Vue构造函数上的option,再通过这里的变更逻辑重新赋值到 VueComponent.options上,这样生成的实例就可以访问到 Vue.mixin注入的属性了。
4.3 使用resolveModifiedOptions获取更改的属性
应对上面说的这个 bug,Vue官方也对这resolveConstructorOptions方法进行了调整。
针对VueComponent构造函数上可能存在的options更改进行了处理。
- 首先在 Vue.extends中保存了VueComponent的当时的options。
1 | Vue.extend = function(){ |
- 使用resolveModifiedOptions查找修改的option部分
1 | function resolveModifiedOptions(Ctor){ |
- 将修改的 options合进 extendOptions
1 | if (modifiedOptions) { |
- 合并 extendOptions以及 superOptions
extendOptions是最新的子类构造函数 options。
superOptions是最新的父类构造函数 options。
将两者合并就不会有问题了。
1 | options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) |
5.4 总结
这个函数可以获取到构造函数上最新的options。
同时可以更新子类构造函数和父类构造函数上的superOptions、extendOptions以及 options
。
五、mergeOptions的合并场景
mergeOptions在实例化时用于合并构造函数的 options和传入的 options。
mergeOptions 是实现继承和实例化的核心函数。
1 | function mergeOptions(parent,child,vm){ |
mergeOptions函数不仅仅用于生成实例的$options属性。
也可以用于子类构造函数和父类构造函数的选项合并。
在多个场景都是用了这个函数。
5.1 Vue.extend
1 | Vue.extend = function(extendOptions){ |
前面我们已经多次提及这个方法了。
这里的 mergeOptions 用于合并父类构造函数选项和传入的选项,生成子类构造函数的选项。
在这里:父类构造函数的选项相当于父类,而传入的选项相当于子类。不传入vm实例。
5.2 Vue.mixin
1 | Vue.mixin = function (mixin: Object) { |
前面我们也多次提到这个方法,这个方法主要是用于合并构造函数的options,和传入mixin选项,生成全新的构造函数的 options。
在这里:构造函数的选项相当于父类,而传入的 mixin选项相当于子类。不传入vm实例。
5.3 vm.$options
1 | vm.$options = mergeOptions( |
该方法用于在实例化的时候生成实例的$options属性。这个方法主要用于合并实例对应的构造函数的选项和传入的选项,生成一个完整地实例选项。
在这里:构造函数的选项相当于父类,而实例化时传入的 mixin选项相当于子类`。`此时传入vm实例。
六、合并策略
在实例化的时候,会将构造函数上的选项和用户传入的选项进行合并,将最终的合并选项注入到实例中。
1 | vm.$options = mergeOptions( |
mergeOptions函数用于合并2个选项,并返回一个新的选项。
1 | function mergeOptions(parent,child){ |
大家可以先思考一下合并2个对象需要注意些什么呢?
假设构造函数的options和传入的options中都存在data,这个时候合并的是:
- 直接覆盖?
- 如果覆盖的话是使用构造函数的data还是传入的data?
- 还是将data中的内容再进行合并呢?
实际上不同的选项中的都存在自己的合并逻辑。
比如data选项和生命周期钩子选项合并策略肯定是不同的。
6.1 自定义策略
在Vue中,可以自己定义不同的合并策略。
1 | // 可以配置合并策略 |
配置完以后,就可以在optionMergeStrategies获取用户配置的策略。
1 | // 源码中定义策略变量首先获取optionMergeStrategies 如果没有配置 optionMergeStrategies 默认是一个空对象 |
然后再在 strats这个对象上添加属性,定义不同的策略。
1 | // data的合并策略 |
由于strats是一个对象,所以你如果定义了和内部定义的策略相同的选项,会被覆盖掉。也就意味着你无法重新定义 data、computed、props等内部策略
但是你可以自定义自己的选项。
1 | import Vue from "vue/dist/vue.esm.browser" |
比如你想要实现你自定义的 customArray选项 数组内容合并并且去重,可以使用使用optionMergeStrategies
:
1 | // parentVal代表构造函数上的选项值, childVal代表传入的选项值 |
我们打印一下这个实例,可以发现已经成功了。
6.2 默认的合并策略
由于可以选项高度可自定义,所以 Vue中内置了一套默认的合并策略。
主要应对没有设置对应策略的合并情况。
1 | const defaultStrat = function (parentVal, childVal) { |
可以看出来默认的合并策略是传入的options直接对构造函数的options进行强制覆盖(如果存在的话)。
6.3 el、propsData的合并策略
可以看出来 el、propsData的合并策略和默认策略一样,只是多了一个开发环境的报错提示。
1 | if (__DEV__) { |
那么这个报错是什么意思呢?
策略函数的的第三个参数vm代表vue实例。
而这个vm同时也是mergeOptions的第三个参数。
而 mergeOptions作为一个通用函数,不仅只是在实例初始化的时候被调用,同时也在Vue.extend、Vue.mixin等多个方法中被使用,这个时候还没有生成 vm实例,所以 vm为空。而 el属性、propsData属性是属于跟实例相关的属性,所以如果在Vue.mixin等方法中注入 el属性,则会报错。
综上所述,这行报错的意思就是禁止在除了实例化之外的地方注入 el选项。
比如调用:
1 | Vue.mixin({ |
控制台就会报错:
6.4 data的合并策略
data选项在 Vue中无疑是使用最频繁的选项之一。
所以它的合并策略相当复杂。
1 | strats.data = function ( |
6.4.1 在定义时报错
我们知道通常在组件中,建议将 data定义成函数,因为如果将 data定义成普通对象,在该组件被引入到多处时,由于 extend函数内部会缓存这个构造函数,所以 data中的数据可能会被多个组件共享,导致 bug的产生。
childVal在这里代表传入的选项,即在组件中定义的选项。
所以会提示你需要将 data定义成函数,并且依旧调用 mergeDataOrFn。
在执行构造函数时创建子类构造函数时,虽然已经有了 vm了,但是并没有传入 mergeOptions。
1 | Vue.extend = function(){ |
所以这个报错是在创建子类构造函数时进行提示的。
6.4.2 mergeDataOrFn
1 | export function mergeDataOrFn( |
该函数生成了一个合并函数。
当这个合并函数执行的时候将获取构造函数的上的data和传入的data- 当传入的选项中存在 data,则调用mergeData合并 2 个data并返回。
- 当传入的选项中不存在 data时,则直接返回构造函数上的 data。
6.4.3 mergeData
当构造函数的选项和传入的选项都存在 data时,需要调用 mergeData对 2 个选项进行合并。
1 | function mergeData( |
这个函数的主要作用是合并两个对象的属性,可以用于初始化组件的数据对象,或者在组件的 created 钩子中合并父组件和子组件的数据。通过 recursive 参数,它可以灵活地进行浅合并或深度合并。
由于其中涉及到响应式数据的内容,所以不在这里深入讨论。
后面会单独出响应式数据的内容。
6.5 生命周期的合并策略
1 | export const LIFECYCLE_HOOKS = [ |
上面这个三元表达式还是比较复杂的。
- 如果子类和父类都拥有相同的钩子选项,则将子类选项和父类选项合并。
- 如果父类不存在钩子选项,子类存在时,则以数组形式返回子类钩子选项。
- 当子类不存在钩子选项时,则以父类选项返回。
- 子父合并时,是将子类选项放在数组的末尾,这样在执行钩子时,永远是父类选项优先于子类选项执行。
简单总结一下:对于生命周期钩子选项,子类和父类相同的选项将合并成数组,这样在执行子类钩子函数时,父类钩子选项也会执行,并且父会优先于子执行。
6.6 组件、指令、过滤器的合并策略
1 | export const ASSET_TYPES = ['component', 'directive', 'filter'] |
很明显 组件、指令、过滤器的合并策略就是直接对构造函数的选项进行覆盖。
Object.create()方法创建一个新对象,使用创建的对象来提供新创建的对象的__proto__。这意味着这个合并策略让内置的一些资源选项变成了原型链的形式。
这样子类必须通过原型链才能查找并使用内置的组件和内置指令。
6.7 watch的合并策略
在使用 Vue 进行开发时,我们经常需要对数据变化做出响应,尤其是当涉及到需要执行异步操作或计算成本较高的任务时,watch 选项就显得尤为高效。
关于 watch 选项的合并策略,它与生命周期钩子的合并有相似之处:如果父组件和子组件有相同的观察字段,它们的 watch 选项将被合并为一个数组。
当监测到字段变化时,父类和子类的监听代码将被同时触发。
与生命周期钩子的处理方式不同,watch 选项在合并后的数组中可以呈现多种形式:它们可以是包含选项的对象,也可以是回调函数,或者是方法名的字符串。
这种灵活性使得 watch 选项能够适应更多的使用场景,无论是简单的数据变化监听还是复杂的异步操作处理。
通过这种方式,Vue 允许开发者在组件和混入中灵活地定义和合并 watch 选项,从而实现精细化的数据监控和管理。
1 | strats.watch = function (parentVal,childVal,vm,key) { |
下面结合具体的例子看合并结果:
1 | var Parent = Vue.extend({ |
简而言之:Vue 在处理 watch 选项的合并时,会将子组件与父组件的 watch 选项合并成一个数组。这个数组中的成员可以是回调函数、包含配置的对象,或者是指向函数的方法名。这样的设计提供了灵活的选项,以适应不同的数据监控需求。
6.8 props methods inject computed合并
在 Vue 的源码设计中,props、methods、inject 和 computed 这些选项被归为一类,并且它们遵循相同的合并策略。
1 | strats.props = |
简而言之。
- 父类没有相应的选项,则直接使用子类的选项
- 当父组件和子组件都提供了这些选项时,子组件的选项会覆盖父组件的选项。这种策略确保了组件能够继承和扩展行为,同时允许子组件通过提供自己的选项来覆盖继承自父组件或混入的选项。
举个例子:
1 | var Parent = Vue.extend({ |
6.9 provide合并
确保了子组件的 provide 选项可以覆盖父组件的同名选项,同时保留了父组件的其他选项。这种设计允许组件在继承和扩展 provide 数据时有更多的灵活性。通过这种方式,Vue 允许开发者在组件树中有效地传递数据,而无需通过每个层级的组件显式地传递 props。
1 | strats.provide = function (parentVal, childVal) { |