最近在作项目的时候碰到了一个奇怪的问题,经过 Vue.mixin
方法注入到 Vue 实例的一个方法不起做用了,后来通过仔细排查发现这个实例本身实现了一个同名方法,致使了 Vue.mixin
注入方法的失效。后来查阅资料发现 Vue.mixin
注入到实例的 methods
方法会被实例中的同名方法替换,而不会依次执行。因而我就有了查看源码的想法,进而诞生了这篇文章~html
本文所用源码版本为 2.2.6vue
首先从 Vue.mixin
这个方法入手,打开 src
目录不难找到 mixin
所在的文件:src/core/global-api/mixin.js
,其内容以下:api
能够看到这只是一层简单的封装,核心内容基本都在 mergeOptions
方法中,因此下面打开这个方法所在的文件:src/core/util/options.js
。注意 mergeOptions
方法是经过 src/core/util/index.js
引入导出的,其源码在 options.js
中,直接看 options.js
就行了。数组
在 options.js
中找到 mergeOptions
方法,内容以下:ide
其主流程大体以下:函数
checkComponents
检查传入参数的合法性,后面再讲具体实现。normalizeProps
方法和 normalizeDirectives
方法对这两个属性进行规范化。extends
属性,这个属性表示扩展其它 Vue 实例,具体参考官方文档。这里为何要检查这个属性呢?由于当传入对象具备该属性时,表示全部的 Vue 实例都要扩展它所指定的实例(Vue.mixin
的功能便是如此),那么咱们在合并以前,须要先把 extends
进行合并,若是 extends
是一个 Vue 构造函数(也多是扩展后的 Vue 构造函数),那么合并参数变为其 options
选项了;不然直接合并 extends
。extends
属性以后,咱们还要检查其 mixins
属性,这个属性的功能参考官方文档。由于若是传入的 Vue 配置对象仍然指定了 mixins
的话,咱们须要递归的进行 merge。mixin
参数了。能够看到经过 mergeField
函数进行了合并,先遍历合并的目标对象,进行合并了;随后遍历要合并的对象,只对目标对象上不存在的属性进行合并操做。那么合并的重点就到了 mergeFiled
函数了。继续看 mergeField
函数:性能
function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) }
该函数经过 key
值在 strats
中选取合并的具体函数,这是一种典型的策略模式,因此咱们看 strats
是如何定义的。测试
options.js
中关于 strats
的定义以下:ui
/** * Option overwriting strategies are functions that handle * how to merge a parent option value and a child option * value into the final value. */ const strats = config.optionMergeStrategies
其中 config
对象来自于 src/core/config.js
,它定义了 config
的全部类型及初始值,固然初始值都仍是一些空数组之类的,因此咱们要在 options.js
中看具体的实现。rest
下面根据 Vue 的配置属性分开讲解不一样的合并方式。
el
的合并方式比较简单,由于它自己
源码以下:
/** * Options with restrictions */ 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
方法以及 propsData
方法(propsData 文档),这是由于这两个属性比较特殊,尤为是 propsData
只在开发环境下才使用,方便测试而已。另一个比较特殊的地方是这二者只能在 new
操做符调用 Vue 构造函数所构造的 Vue 实例中才能存在,因此当 vm
未传递时,会弹出一个警告。
这两个属性的合并方法都是 defaultStrat
,其源码以下:
/** * Default strategy. */ const defaultStrat = function (parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal }
能够看出在 childVal
已定义的时候直接替代 parentVal
。
这个方法在后边还会用到。
data
选项的合并是重中之重,由于 data
在子组件中是一个函数,它返回的也是一个特殊的响应式对象。
其源码以下:
这里分了两种状况,一种是传递了 vm 参数,一种是没传递。
当没传递 vm 参数的时候,须要校验 childVal
是不是函数,而 parentVal
不须要校验,由于它必须是函数才能经过以前的 merge 校验,到达如今这一步。肯定都是函数以后,就调用这两个函数,再而后对返回的两个 data 对象经过 mergeData
作处理,这里后面再讲。
当传递了 vm 参数的时候,须要用其余方式处理,当是函数的时候,使用返回值作下一步合并;当是其余值的时候,直接使用其值进行下一步合并。
这一步要校验 childVal
和 parentVal
是否为函数。正是由于这一步校验了,因此前面所讲的状况就再也不须要校验,为何呢?
咱们能够回头看 mergeOptions
的源码,发现其第三个参数 vm 是可选的,在递归的时候它会把 vm 传递给自身,这就致使当咱们一开始调用 mergeOptions
的时候传递了 vm,则其后全部递归都会传递 vm;当咱们一开始未传递 vm 值的时候,其后全部的递归也不会传递 vm 参数。那么是否有 vm 就取决于咱们最开始调用该函数时所传递的参数是否包含 vm 了。
全局查找 mergeOptions
函数的调用,能够看到有两处:
src/core/instance/init.js
,该文件也定义了 initMixin
方法,用于初始化 Vue 把传递给 Vue 构造函数的配置对象合并到 vm.$options 中。这种状况下会传递 vm,其值为当前正在构造的 Vue 实例。src/core/global-api/mixin.js
,这处才是定义的全局 API。简而言之,Vue 构造函数构造 Vue 实例时,会调用 mergeOptions
而且传递 vm 实例做为第三个参数;当咱们调用 Vue.mixin
进行全局混淆时是不会传递 vm 的。前者对应第二种状况,后者对应第一种状况。
当咱们先构造 Vue 实例的时候,vm 被传递进而执行第二种状况,parentVal
会被校验,因此以后再调用 Vue.mixin
时第一种状况再也不须要校验。
当咱们先不实例化 Vue 而先调用 Vue.mixin
时,会先执行第一种状况的代码,那么会致使 bug 出现吗?答案确定是不会,由于此时 parentVal
为 undefined
,由于 Vue.mixin
调用时 parentVal
的初始值为 Vue.options
,这个对象根本不包含 data 属性。
那么 data 合并的任务主要在 mergeData
函数中了,查看其源码:
能够看到这里遍历了要合并的 data 的全部属性,而后根据不一样状况进行合并:
set
方法进行合并,后面讲 set
。继续看 set
函数:
能够看到 set
也对 target 分了两种状况进行处理。首先判断了 target 是数组的状况,而后若是 target 包含当前属性,那么就直接赋值。接下来判断了 target 是不是响应式对象,若是是的话就会在开发环境下弹出警告,最好不要让 data 函数返回一个响应式对象,由于会形成性能浪费。若是不是响应式对象也能够直接赋值返回,其余状况下就会进一步转化 target 为响应式对象,并收集依赖。
以上大概就是 data 的合并方式,能够看出来若是实例指定了与 mixins 相同名称的 data 值,那么以实例中的为准,mixin 中执行的 data 会失效,若是都是对象可是 mixin 中新增了属性的话,仍是会被添加到实例 data 中去的。
Hooks 的合并函数定义为 mergeHook
钩子,其源码以下:
/** * Hooks and props are merged as arrays. */ function mergeHook ( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> { return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal }
这个比较简单,代码注释也写得很清楚了,Vue 实例的生命周期钩子被合并为一个数组。具体有哪些钩子能够被合并被写在 src/core/config.js
中:
/** * List of lifecycle hooks. */ _lifecycleHooks: [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated' ],
合并 assets (components、filters、directives)的方法也比较简单,下面跳过了。
合并 watch
的函数源码以下:
这一段源码也很简单,注释也很明了,跟生命周期的钩子同样,Vue.mixin
会把全部同名的 watch 合并到一个数组中去,在触发的时候依次执行就行了。
这三项的合并都使用了相同的策略,源代码以下:
这里的处理也比较简单,能够看出来当屡次调用 Vue.mixin 混淆时,同名的 props、methods、computed 会被后来者替代;可是当 Vue 构造函数传递了同名的属性时,会以构造函数所接受的配置对象为准。由于 Vue 实例化时也会调用 mergeOptions 第二个参数即为 Vue 构造函数所接受的配置对象,正如前文所述。
前文有讲到几个辅助函数,好比:checkComponents
、normalizeProps
、normalizeDirectives
。这里简单贴一下源码:
这个函数是为了检查 components 属性是否符合要求的,主要是防止自定义组件使用 HTML 内置标签。
这个函数主要是对 props 属性进行整理。包括把字符串数组形式的 props 转换为对象形式,对全部形式的 props 进行格式化整理。
这个函数也主要是对 directives 属性进行格式化整理的,把原来的对象整理成一个新的符合标准格式的对象。
看到 Vue 的官方文档:自定义选项合并策略,它容许咱们自定义合并策略,具体方式就是替换 Vue.config.optionsMergeStrategies
,也就是前文所提到的那个定义在 src/core/config.js
中的属性。咱们也能够看一下源代码,这一功能在 src/core/global-api/index.js
文件中的 initGlobalAPI
定义。
const configDef = {} configDef.get = () => config if (process.env.NODE_ENV !== 'production') { configDef.set = () => { warn( 'Do not replace the Vue.config object, set individual fields instead.' ) } } Object.defineProperty(Vue, 'config', configDef)
能够看到最后一句给 Vue 函数定义了一个 config
属性,其 property 定义为 configDef
。在生产环境下不容许设置其值,可是在开发环境下,咱们能够直接设置 Vue.config
。那么经过设置 Vue.config.optionsMergeStrategies
,咱们能够改变合并策略,在后面再进行合并操做时,都会读取 config 对象中的属性,这时就可使用咱们自定义的合并策略进行合并了。
看了这些属性的合并方式之后,对 Vue.mixin
的工做方式也有了必定的了解了。我的认为基本上能够把 Vue.mixin
合并属性的方式分为三类,一类是替换式、一类是合并式、还有一类是队列式。
替换式的有 el
、props
、methods
和 computed
,这一类的行为是新的参数替代旧的参数。
合并式的有 data
,这一类的行为是新传入的参数会被合并到旧的参数中。
队列式合并的有 watch
、全部的生命周期钩子(hooks
),这一类的行为是全部的参数会被合并到一个数组中,必要时再依次取出。
因此对于 Vue.mixin
的使用咱们也须要当心,尤为是替换式合并的属性,当你在 mixins 里面指定了之后,就不要再实例中再指定同名属性了,那样的话你的 mixins 中的属性会被替代致使失效。
做者水平有限,文章不免存在纰漏,敬请你们指正。