Vue初始化之合并配置
构造函数Vue在生产环境的第一步就是执行原型链上的_init方法。该方法是在initMixin方法中定义,其中options就是我们调用Vue构造函数的时候传过来的,源代码位置:/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
//初始化代码忽略
}
}
合并选项之前都做了些啥
const vm: Component = this
// a uid
vm._uid = uid++
首先声明vm常量指向当前Vue实例,然后给vm常量定义了一个内部变量_uid作为当前组件的唯一标识,每次初始化组件的时候,_uid依次递增。
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
接下来是性能测试。首先声明了两个变量startTag和endTag。如果是在非生产环境下,且config.performance和mark为true的情况下则执行性能追踪。
config.performance来自于core/config.js中的配置。在Vue官方文档中,我们看到可以对config进行修改配置。例如Vue.config.performance = true的时候,非生产环境主要对以下功能进行追踪:
- 1、组件初始化(component init)
- 2、编译(compile),将模板(template)编译成渲染函数
- 3、渲染(render),其实就是渲染函数的性能,或者说渲染函数执行且生成虚拟DOM(vnode)的性能
- 4、打补丁(patch),将虚拟DOM渲染为真实DOM的性能
mark这里就不赘述了,在工具方法篇中已做介绍
// a flag to avoid this being observed
vm._isVue = true
// merge options
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 {
//暂时忽略
}
首先在 Vue 实例上添加 _isVue 属性,并设置其值为 true。目的是用来标识一个对象是 Vue 实例,即如果发现一个对象拥有 _isVue 属性并且其值为 true,那么就代表该对象是 Vue 实例,这样可以避免该对象被响应系统观测。
接下来是一个if...else分支。即如果选项上带有_isComponent内部选项,则表示这是已经初始过的组件,这里进行了一个优化策略(暂不做详细介绍,后面会再次讲到)。
合并选项都做了些啥
接下来就是我们本篇文章的核心:选项合并策略。不少人对选项的合并往往嗤之以鼻,认为这不是响应式数据的核心,以为仅仅是两个对象合并成一个对象而已,没必要花太多心思去研究,所以不去深究。其实大错特错,深入研究后你才会发现不仅能从中学到不少东西,而且还可以深入了解每个组件的数据结构,这是理解Vue源码的基础。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
选项合并的目的就是将Vue默认配置与用户自定义的options进行合并后返回一个新的Object,并赋值给实例vm的属性$options。本质是将两个对象合并为一个新对象。
合并主要是mergeOptions函数做的事情,它接受三个参数:
- 1、
resolveConstructorOption函数返回的Vue默认配置 - 2、用户自定义的默认配置。若无则传空对象
- 3、当前实例本身
先来看下resolveConstructorOption函数,它的位置也在:core/instance/index.js
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
//暂时忽略
}
return options
}
resolveConstructorOption函数只接受一个参数Ctor,那么这个Ctor是啥呢?在实际调用的时候传的唯一参数是vm.constructor,正常情况下(注意是一般正常理解的情况下)这个值指向的其实就是Vue构造函数本身,举个例子:
function testFn(){
this.a = 1;
}
var vm = new testFn();
console.log(vm.constructor === testFn); // true
所以let options = Ctor.options其实相当于let options = Vue.options。鉴于读者是刚看源码,对Vue.extend还不熟悉,为了便于理解,我们可以暂时这么理解:resolveConstructorOptions(vm.constructor)其实就是Vue.options。等整体看透之后再回头重看的时候,就能理解了。
不过这里还是会先简单介绍一下resolveConstructorOptions函数中if语句主要是干嘛的。
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
}
}
}
我们看到if语句的判断条件是Ctor.super,super是子类才有的属性。举个例子:
var sub = Vue.extend();
var vm = new sub();
console.log(vm.constructor); // sub
console.log(sub.super); // Vue
以上例子也充分说明了,实例vm的constructor指向的不一定是Vue,这种情况指的是sub函数。因是继承而来的,所以sub函数有super属性。
总结:目前只需要知道resolveConstructorOptions返回Vue.options即可,其中的if(Ctor.super)与Vue.extend()方法有关,等到后面遇到的时候再重点分析。
mergeOptions
接下来我们重点看下mergeOptions方法,它接受三个参数。
第一个参数暂时认为是Vue.options:
Vue.options = {
components:{
keepAlive,
transition,
transitinGroup
},
directives:{
model,
show
},
filters:Object.create(null),
_base:Vue
}
第二个参数是我们调用new Vue的时候传的参数,例如:
{
el:'#app',
data(){
return {
name:'wang'
}
},
methods:{
init(){
}
}
}
第三个参数就是当前实例本身vm。所以我们改下mergeOptions方法,其实就相当于:
vm.$options = mergeOptions(
{
components:{
keepAlive,
transition,
transitionGroup
},
directives:{
model,
show
},
filters:Object.create(null),
_base:Vue
},
{
el:'#app',
data(){
return {
name:'wang'
}
},
methods:{
init(){
}
}
},
vm
)
接下来我们详细看下mergeOptions源代码。位置:core/util/options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
//剩余代码暂时忽略
}
首先在非生产环境下会使用checkComponents方法检测我们自定义配置中的组件名称命名是否规范。为何要在非生产环境检测呢?因为在非生产环境检测规范后,我们不大可能在build生产的时候再去修改代码,也就是说生产环境没必要再次检测,以此来达到节约性能的目的。类似的情况process.env.NODE_ENV !== 'production'以后会有很多,就不再一一赘述。检测代码如下:
function checkComponents (options: Object) {
for (const key in options.components) {
validateComponentName(key)
}
}
export function validateComponentName (name: string) {
if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
warn(
'Invalid component name: "' + name + '". Component names ' +
'should conform to valid custom element name in html5 specification.'
)
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
)
}
}
通过以上代码可以看出,检测的原理就是遍历我们传递选项的components属性。若满足以下条件,则打印警告:
- 1、满足正则表达式
^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$ - 2、
isBuiltInTag(name) || config.isReservedTag(name)之一成立的情况下
说人话?好的好的
其实第一条就是限定组件的命名规则由普通的字符和中横线(-)组成,且必须以字母开头。
第二条是检测你的组件名称不能与内置标签(slot、component)冲突,也不能是内置标签。
说了这么多,其实就是保证在合并之前你的组件命名合理合法,作者为了防止开发者犯规,也是操碎了心呐。
允许我们传递的参数是一个函数
接下来的这段代码打破了我们上面说的:我们传递的合并参数是一个对象。其实它也可以是一个函数,这里增加了一个判断,若是函数,则child重新指向它的静态属性child.options。
if (typeof child === 'function') {
child = child.options
}
什么场景下会遇到这种情况呢?其实还是跟Vue.extend()函数有关,这个在后面也会详细讲解。
规范化props、inject、directives
normalizeProps
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
这三个函数是用来规范选项,方便后面合并而做的处理。为什么要这么做呢?以props为例,我们知道Vue允许以下多种写法:
//写法一
const yourComponents = {
props:['yourData']
}
//写法二
const yourComponents = {
props:{
yourData:{
type:Number,
default:1
}
}
}
//写法三
const yourComponents = {
props:{
yourData:{
type:Number
}
}
}
//写法四
const yourComponents = {
props:{
yourData:Number
}
}
这个给开发者提供了非常便利的选择,可以根据自己的习惯任性的写逻辑。但凡事都有两面性,开发者爽了,源码作者就要写更多方法来适应。也就是在真正合并之前,将开发者写的多种格式统一规范,方便后面合并。接下来以normalizeProps为例:
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
我们看到normalizeProps函数接受两个参数:第一个是开发者传递的options配置,第二个是可选的当前组件实例,主要用于非生产环境检测异常(props格式非数组也非对象)后打印警告。
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
//暂时忽略此处代码
options.props = res
}
我们将代码缩减下看看,首先读取当前开发者配置中的props,若不存在则直接返回。之后声明了新对象res,用于保存规范后的结果输出,同时又声明了i、val、name三个变量供后面使用。
function normalizeProps (options: Object, vm: ?Component) {
//暂时忽略
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
我们可以看到,这里执行了三个if分支判断。其中用到了camelize方法,可移步查看camelize
- 若是数组格式,
while循环遍历数组的每个元素。只有元素是字符串格式的时候,改成{type:null}格式。例如开发者写的是props:['yourData'],转换后的结果就是props:{yourData:{type:null}}。若元素非字符串且非生产环境下则打印警告。 - 若是纯对象格式,
for循环遍历对象的每个元素。先保存value结果,在规范命名规则,之后判断若val是纯对象,则直接使用,否则改成{type:val}的格式。比如
// 例子一:
props:{
yourData:Number
}
// 将会被修改为以下格式
props:{
yourData:{
type:Number
}
}
//例子二:
props:{
yourData:{
type:Number
}
}
// 不做格式转变,直接使用。至于是否包含默认值,对转换结果没影响
总结:其实规范化props很简单,只是将数据修改为纯对象格式。对象增加一个type属性,若有指定类型则显示类型,否则为null。若有默认值default也会包含在里面。
normalizeInject
详细了解了如何规范化props之后,再看另外两个规范想必就非常容易了,这里就简单介绍了:
function normalizeInject (options: Object, vm: ?Component) {
const inject = options.inject
if (!inject) return
const normalized = options.inject = {}
if (Array.isArray(inject)) {
for (let i = 0; i < inject.length; i++) {
normalized[inject[i]] = { from: inject[i] }
}
} else if (isPlainObject(inject)) {
for (const key in inject) {
const val = inject[key]
normalized[key] = isPlainObject(val)
? extend({ from: key }, val)
: { from: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "inject": expected an Array or an Object, ` +
`but got ${toRawType(inject)}.`,
vm
)
}
}
这里有一点非常有意思:先定义变量inject缓存options.inject的值,之后定义normalized与options.inject同时指向一个空对象。其中用到了extend方法,可移步查看extend
- 若是数组格式,则遍历改成
{yourKey:{from:yourKey}}格式; - 若是纯对象,若
val是纯对象,则改成{yourKey:{from:yourKey,yourData:yourData}}格式;否则还是{yourKey:{from:yourKey}}格式。
normalizeDirectives
规范化directives
function normalizeDirectives (options: Object) {
const dirs = options.directives
if (dirs) {
for (const key in dirs) {
const def = dirs[key]
if (typeof def === 'function') {
dirs[key] = { bind: def, update: def }
}
}
}
}
当且仅当directives存在且每一项的value值是函数的时候,数据修改前后如下:
// 修改前
{
directives:{
a:function(){}
}
}
//修改后
{
directives:{
a:{
bind:function(){},
update:function(){}
}
}
}
总结:规范化数据格式至此告一段落,它们的存在只是为后面真正的合并做一个规范化处理,保持数据格式统一,开发者的命名规范。
mixins、extends的合并方式
我们知道mixins用于解决代码复用的问题。接下来的这段代码就是将开发者配置中可能存在的mixins混入合并到parent中。
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)
}
}
}
首先判断!child._base的情况下才会执行上面代码。而child是开发者传递的配置,默认肯定是没有的,那肯定会执行里面的内容。那么什么情况下才会有child._base呢?我们知道_base只有Vue.config默认配置中有这么一个属性,且指向的是构造函数Vue本身,而合并是取两个对象的最大值为一个新对象,所以合并后的vm.$options._base结果肯定是有的了。所以结论是:只有第一次合并,是原始合并选项,不是另一次mergeOptions的结果再合并的时候,才会执行这里的代码。这么做是防止重复执行,一方面是节省性能,另一方面也是没必要。
- 若开发者配置中存在
mixins,则遍历mixins中的每个元素,递归调用mergeOptions方法合并产生一个新的对象,并赋值给parent; - 而若开发者配置中存在
extends,则更简单,因为extends只是一个对象,相当于mixins的一个元素,直接递归调用即可。
主要选项的合并方式
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
做了这么多铺垫,现在才轮到真正合并的阶段。合并的原理并不复杂:
- 首先创建一个用于最终输出结果的
options对象; for循环遍历parent合并到options中;for循环遍历child,只有parent中没有的key对象才能合并到options中(防止开发者覆盖默认配置);mergeField函数是核心合并方式。在这之前先定义了strats策略对象,对象上分别定义了el、propsData、data、生命周期、components、directives、filters、watch、props、methods、inject、computed、provide的合并方式,称为选项合并策略,即不同的模块采用不同的合并方式;- 若找不到指定合并方式,例如开发者定义了一个特殊的选项:
child.aabbcc = {},这个时候strats.aabbcc的结果是undefined,这种情况则会调用默认合并方法defaultStrat。 - 默认合并方法是在没指定合并策略的前提下使用的,若开发者定义了一个特殊的选项
child.aabbcc={},我们也可以提前在全局配置下定义同名合并方法:Vue.config.optionMergeStrategies.aabbcc = function(){}。这就是Vue文档上提到的自定义选项合并策略
接下来我们来一一查看各个模块的合并方法。
el、propsData 合并策略
if (process.env.NODE_ENV !== 'production') {
strats.el = strats.propsData = function (parent, child, vm, key) {
if (!vm) {
warn(
`option "${key}" can only be used during instance ` +
'creation with the `new` keyword.'
)
}
return defaultStrat(parent, child)
}
}
在非生产环境定义了strats.el、strats.propsData,其最终调用的还是默认合并方法defaultStrat。这里有人就要奇怪了,为何只在非生产环境定义,生产环境怎么办?其实生产环境也是调用的默认合并方法defaultStrat,因为strats.el、strats.propsData的结果是undefined,没有的话就会采用默认合并策略。
其实Vue中无论哪个环境,其最终输出结果必定是一致的。如果实现的过程有区别,那一定是为了方便开发调试,这里唯一的区别是多了一个if(!vm){}判断没有vm实例的情况下打印警告,提示el选项或者propsData选项只能在使用new操作符创建实例的时候可用。这也说明了,如果拿不到vm则说明处理的是子组件选项。
defaultStrat 默认合并策略
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
接下来就是策略合并的默认方法defaultStrat。当一个选项不需要特殊处理的时候,就使用默认合并策略。逻辑很简单:若childVal存在则直接返回,否则返回parentVal。
data 合并策略
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
data的合并策略代码如上所示,其最终主要使用mergeDataOrFn方法处理,有三种可能性:
- 如果是子组件选项,且开发者所写的
data不是函数的情况下(data必须是函数)则不做合并处理,直接返回parentVal结果; - 如果是子组件选项,且开发者所传
data格式合规(是函数)的情况下,直接调用mergeDataOrFn方法处理data结果; vm存在的情况下,也就是当new Vue的时候(因为这个时候vm值是必然存在的),也直接调用mergeDataOrFn方法处理data结果。
总结:子组件与根组件合并data选项都是调用了mergeDataOrFn方法处理,唯一的区别是是否传vm参数。
那么mergeDataOrFn方法到底是怎么处理的呢?我们先来看下源码:
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn () {
// instance merge
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
通过上面代码我们可以看出mergeDataOrFn分为两种情况:
情况一:不存在vm属性的时候,说明处理的是子组件选项。
- 根据注释可以得知:当前是调用
Vue.extend函数时进行合并处理的,要求此时处理的父子data都必须是函数类型。 - 接下来是两个
if判断。如果没有childVal,说明子组件选项中没有data选项,则直接返回父组件选项。同样的,若负组件中不存在data选项,则无需合并,直接返回子组件data选项。 - 当
parentVal、childVal都存在的情况下才会真正执行data的合并策略,直接返回一个mergedDataFn方法,此时data的合并代码直接执行结束,返回的是一个函数mergeDataFn。所以:data原本是一个函数,合并后仍然是一个函数,而不是一个纯对象。 mergeDataFn方法内部返回的是函数mergeData执行后返回的结果。而mergeData的两个参数则是子父data方法执行后返回的纯对象格式。
情况二:存在vm属性的时候,说明处理的是非子组件选项,也就是处理new操作符创建实例的情况。
- 此时也是返回的一个未执行的函数
mergedInstanceDataFn - 执行
childVal与parentVal方法分别得到对应的纯对象instanceData与defaultData。如果子类data方法存在,则直接调用mergeData方法合并两个对象为一个纯对象作为mergedInstanceDataFn函数的返回值,若不存在则直接返回父类对象作为mergedInstanceDataFn函数的返回值。
总结:mergeDataOrFn方法无论是否有vm属性,最终返回的永远是一个未执行的函数。内部只是调用了mergeData方法,将父子data函数执行后得到的纯对象合并之后得到合并后的纯对象。
上面提到了mergeData方法,那么这个方法是如何工作的呢?我们继续看它的源码:
function mergeData (to: Object, from: ?Object): Object {
if (!from) return to
let key, toVal, fromVal
const keys = hasSymbol
? Reflect.ownKeys(from)
: Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
// in case the object is already observed...
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
set(to, key, fromVal)
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
return to
}
此方法接受两个参数to、from,后者非必需,若不存在,则直接返回to。从mergeDataOrFn函数中mergeData执行时的传参顺序看,to相当于childVal函数返回的对象,from相当于parentVal函数返回的对象。mergeData方法的作用是遍历from对象数据合并到to上,最终返回to对象。知道整体逻辑后,我们再详细拆解:
- 若
from不存在,那就没有合并的必要了,直接返回to; - 声明三个未赋值的变量
key、toVal、fromVal; - 获取
from对象的key值组成的数组并赋值给keys。至于怎么获取,这里做了个判断,若宿主环境支持原生symbol、Reflect,则使用Reflect.ownKeys获取,否则使用Object.keys方法获取; - 遍历对象,如果发现
key值是__ob__,则跳过继续执行下一个循环。__obj__是啥?是响应式观测数据,后面会详细讲到,这里只要知道__ob__属性不会被合并即可; - 如果
from对象中的key值不存在to中,则调用set函数对to设置对应的值; - 如果
from对象中的key值存在to中,且from[key]与to[key]不全等,且两者都是纯对象的情况下,则递归调用mergeData深度合并。
mergeData函数中用到了set函数,根据引用路径得知这个函数的位置:core/observer/index.js。里面的逻辑较多,后面在讲到响应式的时候会详细解释,目前我们只提取当前用到的代码,方便大家理解:
export function set (target: Array<any> | Object, key: any, val: any): any {
//暂时忽略
const ob = (target: any).__ob__
//暂时忽略
if (!ob) {
target[key] = val
return val
}
//暂时忽略
}
生命周期选项合并策略
源码中strats.data...合并策略之后就是生命周期选项的合并策略,源码如下:
// Hooks props 最终都会被合并为数组格式
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
//以下代码来自 src/shared/constants.js
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
从上面代码可以很容易看出:遍历LIFECYCLE_HOOKS数组,将生命周期钩子函数每一项挂到strats策略对象上,全部指向mergeHook函数。这说明合并生命周期选项的核心就是mergeHook函数,整个函数体由三组三目运算符组成。我们接下来拆解看看mergeHook函数都做了些啥:
mergeHook函数接受两个参数,第一个是父类生命周期钩子,第二个是子类生命周期钩子;childVal、parentVal都存在的情况下,则res是他们合并后的数组结果。注意:parentVal在前;childVal存在,parentVal不存在的情况下,这个时候若childVal是数组则赋值给res,否则将childVal作为唯一元素组成数组后赋值给res;- 若
childVal不存在,则直接将parentVal赋值给res; mergeHook函数最后返回一个数组,若res为true,即存在的情况下,则返回去重后的数组;
学习了生命周期合并原则后,我们发现了一个新的好玩的东西:生命周期不仅仅可以写成一个函数,还可以写成函数组成的数组格式。
// 平时我们会这么写
{
//代码忽略
created:function(){
console.log('created')
}
//代码忽略
}
//其实也可以这么写
{
//代码忽略
created:[
function(){
console.log('created1')
},
function(){
console.log('created2')
}
]
//代码忽略
}
总结:生命周期合并最终都会被合并成一个数组的格式,他们并不会被相互替换。会本着父辈在前,子类在后的原则。实际执行的时候,也是本着这个原则。
assets 选项合并策略
function mergeAssets (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): Object {
const res = Object.create(parentVal || null)
if (childVal) {
process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
return extend(res, childVal)
} else {
return res
}
}
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
//以下代码来自 src/shared/constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
通过ASSET_TYPES的内容我们可以看到,Vue中components、directives、filters被认为是资源。与生命周期合并原则类似,遍历ASSET_TYPES分别在strats上指定合并策略方法是mergeAssets。此方法的逻辑也很简单:
- 创建一个原型为
parentVal的对象res; - 若子类不存在,则直接返回
res; - 若子类存在,则直接将
childVal遍历复制到res中,最后直接返回res。
其中,非生产环境调用了assertObjectType方法,源码如下:
function assertObjectType (name: string, value: any, vm: ?Component) {
if (!isPlainObject(value)) {
warn(
`Invalid value for option "${name}": expected an Object, ` +
`but got ${toRawType(value)}.`,
vm
)
}
}
其目的就是在非生产环境下检测childVal,确保其为纯对象,若不是则给出警告。
总结:静态资源的合并就是先创建一个父辈为原型的空对象,将子类合并到空对象后返回
watch 合并策略
顺着源码位置继续往下,assets选项合并后就是watch选项合并,源代码主要如下:
strats.watch = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
/* istanbul ignore if */
if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = {}
extend(ret, parentVal)
for (const key in childVal) {
let parent = ret[key]
const child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
}
- 首先确保父子都不是火狐浏览器对象原型链上的
watch,若是,则置空。 - 若子类不存在,则直接返回原型为父类的空对象;
- 接下来是在非生产环境下检测子类是否是纯对象,若不是则给出警告;
- 如果父类不存在,则直接返回子类;
- 接下来就是父子都存在的情况下的合并。首先将父类遍历合并到空对象
ret上。接下来就是遍历子类每一项,检测父类是否包含同名选项,若有则需确保父类同名选项为数组格式。 - 最后将子类每一项
复制到ret对象中,若与父类名称冲突则返回与父类合并后的新数组,若不冲突则返回当前选项元素组成的新数组。 总结:合并后的watch选项,若父子存在同名,则同名元素的值为数组格式,否则还是一个函数
props、methods、inject、computed 合并策略
watch合并源代码后是这四个家伙的合并策略,期源代码逻辑如下:
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
if (childVal && process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
if (!parentVal) return childVal
const ret = Object.create(null)
extend(ret, parentVal)
if (childVal) extend(ret, childVal)
return ret
}
合并原理很简单:
- 首先是非生产环境检测子类是否是纯对象,若不是则打印警告;
- 若父类没有,则直接返回子类;
- 创建一个没有原型的空对象,将父类对应的内容遍历拷贝到空对象
ret中; - 若子类也存在,则将子类内容遍历拷贝到
ret中; - 最后返回拷贝后的对象
ret。
总结:这四个选项的合并原则很简单,就是创建一个没有原型的纯对象,遍历拷贝父子类到新对象即可。
provide 合并策略
strats.provide = mergeDataOrFn
最后就是provide的合并策略,合并方法就是mergeDataOrFn。前面已经讲过,这里不再赘述。