如果您是刚开始准备阅读Vue.js的源码,建议先看一下本系列的Vue.js源码学习 —— 起步,相信会对您后面的阅读有很大帮助。
这篇是接着上一篇Vue.js源码学习 —— 合并选项前的预处理,也建议您先看一下,然后再来看这篇会轻松很多。
这篇文章要看的代码主要位于文件core/util/options.js
中。
我没有在文章中大量的贴源码,一定要将源码clone到本地哦。
预备知识
Vue.options
上一篇Vue.js源码学习 —— 合并选项前的预处理中有说过Vue.options
的内容,这里再复习一下,为了便于在举例子时说明结果:
1 | { |
extend函数
需要了解工具函数extend
的实现,因为在合并的过程中会经常使用到。extend
函数位于文件shared/util.js
中,代码如下:
1 | /** |
extend
函数接收两个对象to
和from
,并将from
中的键值对混入到to
中,最后返回to
。这背后的几点含义:
- 在
to
和from
中有相同key
时,from
中的值会覆盖to
中的值。 to
中不在from
中的key
仍然保留。from
中不在to
中的键值对,会被添加到to
中。
所以下面的合并选项中,如果用到了extend
函数的,就是采用了覆盖的策略。
各个选项的合并策略
从文件的最上面开始看吧。
自定义合并选项的策略
1 | /** |
config.optionMergeStrategies
是提供给开发者使用的,我们可以自定义父选项和子选项要怎样合并,通常用于自定义的选项的合并。可以在官方文档#自定义选项合并策略一节了解具体如何使用。
el和propsData选项的合并策略
1 | /** |
el
和 propsData
两个选项只能用于Vue
的实例对象中,也就是说不能直接在Vue
或其子类的构造器选项中设置el
和 propsData
,开发环境下会给出警告信息。
propsData
用于在创建实例时传递 props。主要作用是方便测试。
data选项的合并策略
data
选项此时并没有真正执行合并,只是返回了一个函数,真正地合并过程在该函数中。当合并发生在创建子类构造器时,会返回mergedDataFn
函数,当合并发生在创建实例对象时,会返回mergedInstanceDataFn
函数。
这相当于是延迟了合并
data
选项。因为data
中的计算有可能依赖了props
选项中的内容,所以需要在初始化props
之后,才能去真正地执行data
的合并。
先从下面这段代码开始看:
1 | strats.data = function ( |
在vm
有值时,合并发生在创建实例对象时,在vm
没有值时,也就是没有传vm
时,合并发生在创建子类构造器时。我们知道创建子类构造器使用Vue.extend
,看一下它的实现,发现确实是没有传的:
1 | Sub.options = mergeOptions( |
而在Vue.prototype._init
中始终都会传vm
。
当合并发生在创建子类构造器时,那data
选项的类型必须是一个函数,如果不是,开发环境下会给出警告信息。
为什么此时
data
选项的类型必须是一个函数呢?
通过前面的分析,我们知道构造器的选项会与其所有实例对象的选项合并,就会导致data
在所有的实例对象中共享。如果data
是一个普通对象,就会导致一个使用了该组件的实例对象改变了data
中的值时,其他使用该组件的实例对象获取到的data
也会受影响。data
是函数时,所有实例对象拥有的是函数返回值的副本,所以不会有影响。
再接着看mergeDataOrFn
函数,在vm
没有值,也就是合并发生在创建子类构造器时,是直接将childVal
或parentVal
返回的,并没有用函数来包装,所以开发者此时一定要传函数。
还是通过例子一步一步来看,先举个合并发生在创建子类构造器时的例子:
1 | // data option in constructors |
此时parent
是Vue
构造器的options
,因为Vue.options
中没有data
选项,所以parentVal
是undefined
,childVal
是我们给Parent
传的data
,因此执行了mergeDataOrFn
函数中的下面这段:
1 | if (!parentVal) { |
接着上面,再加上一个继承自Parent
的子类:
1 | const Child = Parent.extend({ |
经过上一步的合并后,此时Parent.options.data
有值了,也就是parentVal
有值了。但是因为我们没有在Child
中传data
选项,所以childVal
为undefined
,此时执行了mergeDataOrFn
函数中的下面这段:
1 | if (!childVal) { |
现在给上面的Child
传个data
选项:
1 | const Child = Parent.extend({ |
就会执行到下面的语句:
1 | // when parentVal & childVal are both present, |
此时data
返回了mergedDataFn
函数。该函数中又调用了mergeData
函数,传给它的参数,如果data
是函数,就是data
执行后的返回结果。如果data
是对象,就是该对象。总之传给mergeData
函数的是两个对象了。
再看当合并发生在创建实例对象时,直接举个parentVal
和childVal
都有值的例子:
1 | // data option in instance object |
此时data
返回的是mergedInstanceDataFn
函数。该函数也是调用了mergeData
函数,给它的传参处理逻辑与构造器是很像,区别在于如果data
是函数,在mergedDataFn
函数中调用data
传的是this
,而mergedInstanceDataFn
函数中调用data
传的是vm
。所以data
选项在是函数时,还可以接收一个参数。
再看mergeData
函数内部的实现:
1 | function mergeData (to: Object, from: ?Object): Object { |
整个函数做的事情就是将from
对象中的属性合并到to
对象中,最后将to
返回。先获取from
对象中的所有keys
,然后分了几种情况处理:
如果
key
正在被观察,不做处理。如果
key
不在to
对象中,将调用set
函数将该属性设置到to
对象中。这个set
函数位于observer/index.js
中,因为里面牵扯到响应系统的东西,所以暂时先不细看。如果
key
在to
对象中:- 如果在
from
中和to
中的属性值都是对象,并且没有指向一个相同的对象,就递归调用mergeData
函数进一步合并。 - 其他情况时(属性值是原始值或数组等)都保留了
to
对象中的。
- 如果在
举个例子,可以放到浏览器中调试看看执行的过程:
1 | // mergeData test |
合并后vm.$options.data
如下:
1 | { |
实例生命周期钩子选项的合并策略
从下面这段代码开始看:
1 | LIFECYCLE_HOOKS.forEach(hook => { |
LIFECYCLE_HOOKS
在shared/constants.js
文件中:
1 | export const LIFECYCLE_HOOKS = [ |
这里列出了所有的实例对象生命周期的钩子函数,这表示所有的钩子函数用的都是一样的合并策略。在构造器中有这些钩子函数时,而开发者又传入了同样的钩子函数,会将两个一样的钩子函数合并成一个数组,重新作为选项的值。即使构造器中没有相同的钩子函数,也会将传入的钩子函数放入数组中,重新作为选项的值。
实现实例对象生命周期钩子选项的合并策略的代码如下:
1 | function mergeHook ( |
mergeHook
函数会将parentVal
和childVal
进行合并,返回的始终是一个放钩子函数的数组。dedupeHooks
函数是对数组去重的。parentVal
是来自构造器的选项,从它的类型注释上可以看出,它有值时肯定是一个数组。还是来举例说明这是什么情况下出现的。
1 | const VueChild = Vue.extend({ |
此时parentVal
是Vue.options.created
,所以是undefined
。而childVal
就是我们上面传的created
,经过mergeHook
函数合并后,VueChild.options.created
变成下面这样:
1 | { |
然后我们再创建一个VueChild
的实例对象:
1 | const vm = new VueChild({ |
此时parentVal
是VueChild.options.created
,所以parentVal
一旦有值就是已经合并过了,变成了一个数组。此时childVal
还是我们上面传的created
。这样合并后vm.options.created
就变成了:
1 | { |
资源选项的合并策略
从下面这段代码开始看:
1 | ASSET_TYPES.forEach(function (type) { |
ASSET_TYPES
定义在shared/constants.js
文件中:
1 | export const ASSET_TYPES = [ |
所以这里的资源选项是指:components
、directives
和 filters
。这几个选项都是哈希表,并使用一样的合并策略。将构造器中的选项作为合并后的选项的原型,开发者传入的选项正常混入到合并后的选项中。我们已经知道了Vue.options
中一开始就有这几个选项,所以它们会被合并到所有实例对象或子类构造器的这个几个选项的原型中。这就使得内置的组件和指令不需要开发者显示地注册,就能正常使用了。
如果传了这几个选项,必须是对象类型的,否则在开发环境下会给警告。
实现资源选项合并策略的代码如下:
1 | /** |
const res = Object.create(parentVal || null)
也就相当于res.__proto__ = parentVal || null
。
下面还是通过举个例子一步一步来看一下合并过程,你可以直接放在浏览器中打断点看看哦。
1 | // merge components options |
这里parentVal
是Vue.options.components
,childVal
是undefined
,因为我们没有传入components
选项。经过mergeAssets
函数合并后,ButtonCounter.options.components
此时为:
1 | { |
再使用ButtonCounter
组件创建一个Vue
实例对象:
1 | const vm = new Vue({ |
此时还是Vue.options.components
与我们传入的components
选项合并。因为Vue.component
的作用就是注册一个全局组件,所以现在Vue.options.components
中多了一个button-counter
组件,经过mergeAssets
函数合并后vm.options.components
现在为:
1 | { |
这里举的是components
的例子,directives
和filters
也是一样的合并过程。
这也就解释了在合并选项前的预处理#传参一节中说到的,与Vue.options
合并后的app.$options
中的components
、directives
的样子。
watch选项的合并策略
watch
选项也是一个哈希表,key
是需要观察的表达式,value
是对应回调函数。value
也可以是方法名,或者包含选项的对象。
watch
选项的合并策略是,在开发者传入的watch
选项中的key
如果和构造器的watch
选项中的key
一样时,会将两者合并成一个数组,并重新作为该key
对应的value
。这就是使得在被观察的key
发生改变时,所有watch
该key
的地方都会得到执行。
实现watch
选项的合并策略的代码如下:
1 | /** |
nativeWatch
的定义在core/util/env.js
中,内容如下。看注释也可以知道因为火狐浏览器中的Object
原型上本来就有watch
函数,为了跟Vue中的watch
选项不混淆,当组件选项是浏览器原生的watch
函数时,就重置为undefined
。
1 | // Firefox has a "watch" function on Object.prototype... |
剩下的代码明面意思不难看懂,我们还是通过例子来看一下每种情况合并后的结果。先看parentVal
是undefined
,childVal
有值的情况:
1 | // merge watch options |
上面的例子在合并watch
选项时,parentVal
是Vue.options.watch
,所以是undefined
的。childVal
是我们传入的watch
选项。此时会执行if (!parentVal) return childVal
将childVal
直接返回,所以合并后VueChild.options.watch
如下:
1 | { |
再接着看parentVal
有值,但是childVal
是undefined
的情况,沿用上面的例子:
1 | const vm = new VueChild({ |
此时parentVal
是VueChild.options.watch
,肯定有值啦,childVal
是我们传入的选项,上面的例子中没有传watch
选项,所以childVal
是undefined
的。这会执行if (!childVal) return Object.create(parentVal || null)
,返回以parentVal
为原型创建的对象。合并后vm.$options.watch
为:
1 | { |
现在给上面的例子中传入一个watch
选项,就会使得parentVal
和childVal
都有值了:
1 | const vm = new VueChild({ |
这时会先将parentVal
中的内容放入结果中,然后再将childVal
中的内容混入进去,在混入的过程中,childVal
中的属性值都会变成数组,如果与parentVal
中有重复的就合并在一个数组中。parentVal
中没有与childVal
中重合的部分自然就还保留是函数(在“预备知识”中介绍过extend
函数的作用了)。上面的例子合并后vm.$options.watch
为:
1 | { |
props、methods、inject、computed选项的合并策略
props
、methods
、inject
、computed
这几个选项也是哈希表,并且用的是一样的合并策略。在构造器和开发者传入的这些选项中,如果有相同的key
,会用实例对象中该key
的值覆盖掉构造器中该key
的值。
这几个选项的类型必须是对象,否则在开发环境下会给出警告。
props
和inject
虽然允许开发者设置为数组,但是前面在选项规范化的时候,Vue内部已经将其规范成了对象。
代码中实现这几个选项的合并策略的代码如下:
1 | /** |
上面的代码不多解释了,我们还是直接将其对应到实际开发中的场景,举个例子:
1 | // merge props |
此时parentVal
来自Vue.options.props
,所以是undefined
。childVal
是我们上面传的props
选项,是有值的。此时执行if (!parentVal) return childVal
,直接将childVal
作为结果返回。所以合并后的VueChild.options.props
如下:
1 | { |
接着上面的例子,创建一个VueChild
的实例对象:
1 | const vm = new VueChild({ |
此时parentVal
来自VueChild.options.props
,已经有值了。childVal
是我们传入的props
选项,也是有值的。此时会先创建一个空对象,先将parentVal
中的键值对放入进去,然后再将childVal
中的键值对混入进这个对象。此时得到vm.$options.props
如下:
1 | { |
这里举的是props
选项的例子,其他几个选项的合并过程也一样。
provide选项的合并策略
provide
选项的合并使用的是mergeDataOrFn
函数,这个在说data
选项时已经介绍过了,就不赘述了。
选项的合并基本就这些了,基本覆盖了在官方API文档列出的所有选项。在说每个合并策略时,我都举了简单但却能说明问题的例子,希望你能到浏览器中真的去调试一遍,这样加深理解。我们也留了很多很多坑在里面,看后面的代码再慢慢补上。
本文作者:意林
本文链接:http://shinancao.cn/2020/01/10/Vue-Source-Code-02/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!