mixin
的意思是混入,是指将事先配置的选项混入到组件中,然后与组件中的对象和方法进行合并,也就是对组件进行了扩展,也可以理解为是将一段重复的代码进行抽离,然后通过混入的形式达到复用的效果,它有两种混入形式,分别是 Vue.mixin({})
全局注册和组件的 mixins
选项,那么在 Vue 中,他们是怎么进行合并,具体实现是怎么样呢,这篇文章将进行讲解,相信你一定会有所收获;
首先是入口文件,在全局 api 的初始化文件中,通过调用 initMixin
进行注册:
// src/core/global-api/mixin.js
import { mergeOptions } from '../util/index'
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
接着看 mergeOptions
的具体实现:
// src/core/util/options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 校验 mixin 组件属性的规范
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
// 规范化 props / inject / directives
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// _base 是标识 extends 和 mixins 是属于子选项的,确保它不是 mergeOptions 的结果
// _base 在 initGlobalAPI 时给 Vue 本身注入的一个标识 Vue.options._base = Vue
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)
}
}
}
// 定义一个变量,存放 merge 后的结果
const options = {}
let key
// 先遍历前者的选项,合并到 options 里
for (key in parent) {
// 合并的主要核心,通过策略模式来进行合并
mergeField(key)
}
for (key in child) {
// 如果后者里面还存有前者没有的选项,则进行合并
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
// 根据 key 值来确认采用何种策略
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
mergeOptions
方法中可以看出它大致分为几个步骤:
components
选项;props
、inject
、directives
进行规范化处理;mixins
和 extends
选项,有则递归进行合并;options
,作为 merge
的结果集;options
;options
;options
;从上面的代码来看,主要的核心点在于策略模式,也就是对象和方法之间的合并规则,我们接着一个一个看:
data 属性合并
// src/core/util/options.js
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// 如果后者的 data 属性不是一个 function 的形式返回,则直接返回前者的 data
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
}
// 调用 mergeDataOrFn 进行合并
return mergeDataOrFn(parentVal, childVal)
}
// 调用 mergeDataOrFn 进行合并
return mergeDataOrFn(parentVal, childVal, vm)
}
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// 后者没有 data 属性,则直接返回前者的 data
if (!childVal) {
return parentVal
}
// 前者没有 data 属性,则直接返回后者的 data
if (!parentVal) {
return childVal
}
// 当两者都存在时,我们需要返回一个函数,该函数返回两个函数合并后的结果
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 () {
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
// 如果后者存在 data 属性,则返回 merge 之后的结果,否则直接返回前者的 data
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
// to 表示后者的 data,from 表示前者
function mergeData (to: Object, from: ?Object): Object {
// 前者没有,则直接返回后者的 data
if (!from) return to
let key, toVal, fromVal
// 获取 data 中的 key
const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from)
for (let i = 0; i < keys.length; i++) {
key = keys[i]
// 如果该属性已经被观察,则直接下一步
if (key === '__ob__') continue
// 根据 key 获取对应的值
toVal = to[key]
fromVal = from[key]
// 如果后者没有该变量,则直接加到后者的 data
// 如果两个值都是对象,但是值不相等,则进行对象的合并
if (!hasOwn(to, key)) {
set(to, key, fromVal)
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal)
}
}
// 返回合并的结果
return to
}
data
属性的合并主要是 mergeDataOrFn
和mergeData
:
mergeDataOrFn
的实现很简单,主要是判断两者之间是否有一个没有 data
属性,是则直接返回有 data
属性的一方,否则返回一个两者合并结果的函数;mergeData
是两者合并的过程:
data
属性;key
值相等,但是值不相等,则以后者的为准;生命周期相关的合并
// src/shared/constants.js
// 定义一个数组存放生命周期钩子函数的 key
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
// src/core/util/options.js
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
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
}
生命周期钩子函数的合并比较简单,先判断后者的生命周期钩子函数是否存在,是则将后者的相应生命周期钩子函数拼接到前者后面,否则以数组的形式返回后者的相应生命周期钩子;
components、directives、filters 的合并
// src/shared/constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
// src/core/util/options.js
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
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
}
}
components
、directives
、filters
的合并是先拷贝一份原先的属性对象,然后对拷贝的对象进行扩展,它会遍历传入的对象,将传入对象的属性赋值给拷贝对象,如果有重复的,则后者覆盖前者;
watch 合并
// src/core/util/env.js
export const nativeWatch = ({}).watch
// src/core/util/options.js
strats.watch = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
// 如果后者没有 watch 属性,则返回一个创建的对象
if (!childVal) return Object.create(parentVal || null)
if (process.env.NODE_ENV !== 'production') {
assertObjectType(key, childVal, vm)
}
// 如果前者没有 watch 属性,则返回后者的 watch
if (!parentVal) return childVal
const ret = {}
// 将前者的 watch 赋值给 ret
extend(ret, parentVal)
for (const key in childVal) {
let parent = ret[key]
const child = childVal[key]
if (parent && !Array.isArray(parent)) {
parent = [parent]
}
// 将后者的 watch 拼接到前者的 watch
ret[key] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child]
}
return ret
}
watch
的合并和生命周期钩子函数有点相似,都是把后者的属性拼接到前者属性的后面;
props、methods、inject、computed、provide 的合并
// src/core/util/options.js
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
}
strats.provide = mergeDataOrFn
props
、methods
、inject
、computed
的合并都是先定义一个对象 ret
,先遍历前者的属性或方法,对 ret
进行扩展,如果后者有相应的props
、methods
、inject
、computed
等属性,则将后者的覆盖前者的属性或方法;
provide
则是跟 data
合并一样,调用 mergeDataOrFn
进行合并;
默认策略合并
// src/core/util/options.js
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined ? parentVal : childVal
}
如果存在策略对象 strats
没定义的策略,则采用默认策略,默认策略是指如果后者有值,则直接返回后者,否则返回前者;
总结
mixin
是平常开发中常见的一种代码复用手段,它的作用类似于 react
中的高阶组件,mixin
具体实现是通过采用策略模式来将数据进行合并:
data、provide
:后者的值将对前者的值进行扩展,相同属性名(非对象)则以后者的属性值为准,如果两者的值是对象,但值不相等,则继续进行合并,components、filters、directives
:对前者的属性进行拷贝扩展,属性相同则后者覆盖前者;watch
:与生命周期钩子函数类似,将后者的 watch
拼接到前者的 watch
后面;props、methods、inject、computed
:定义一个对象 ret
,遍历前者的属性或方法,对 ret
进行扩展,再遍历后者的属性或方法,后者将覆盖前者的属性或方法;