使用vue开发过程中,常用的抽离公共逻辑代码的方式有两种,一种是不依赖Vue实例的纯数据处理代码,可以写成utils工具方法;一种是依赖Vue实例的逻辑代码,则使用混入(Mixin)。混入虽然好用,但是随着项目代码增多,其管理和维护的成本极速增大,牵一发而动全身,Vue3.0 Composition API 便是试图解决这类问题,感兴趣的同学可以阅读浅尝vue3.0,万字总结初步了解。
本文使用Vue版本2.6.2进行分析,例子来自于Vue官方教程。
混入的使用方法
Mixin的使用方法非常简单,它可以混入包含任意组件选项的对象至目标组件中。在未定义“混入”规则的情况下,Vue本身会采取默认的规则将这些选项“合并”至目标组件中。
举个例子:
// 定义一个混入对象
var myMixin = {
created () {
this.sayHello()
},
methods: {
sayHello: function () {
console.log('hello mixin')
}
}
}
// 定义一个使用混入对象的目标组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello mixin"
当目标组件和待混入的对象含有同名组件选项时,vue默认的“合并规则”如下:
data选项合并
data对象的内部会进行递归合并,当发生冲突时优先使用目标组件的数据。
例子
var mixin = {
data: function () {
return {
message: 'hello',
foo: 'abc'
}
}
}
new Vue({
mixins: [mixin],
data: function () {
return {
message: 'goodbye',
bar: 'def'
}
},
created: function () {
console.log(this.$data)
// => { message: "goodbye", foo: "abc", bar: "def" }
}
})
源码分析
从源码中我们可以看到,数据对象会递归的执行合并操作,如果出现相同则不进行合并,即使用目标组件自身的数据
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]
// 具有可观察对象实例,直接跳过,不需要更换
if (key === '__ob__') continue
toVal = to[key]
fromVal = from[key]
// 属性名相同时不进行赋值操作,即使用目标组件的值
if (!hasOwn(to, key)) {
// 新增属性,使用set方法将其变为响应式
set(to, key, fromVal)
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
// 对象递归执行
mergeData(toVal, fromVal)
}
}
return to
}
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
}
}
}
}
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)
}
生命周期钩子函数
同名的钩子函数会合并成一个数组,混入对象的钩子函数先执行。
例子
var mixin = {
created: function () {
console.log('混入对象的钩子被调用')
}
}
new Vue({
mixins: [mixin],
created: function () {
console.log('组件钩子被调用')
}
})
// => "混入对象的钩子被调用"
// => "组件钩子被调用"
源码分析
- 目标组件没有这个钩子函数,直接使用混入的钩子函数
- 目标组件有钩子函数:
- 如果混入对象也该钩子函数,则混入对象的钩子函数拼接目标组件的钩子函数为一个数组
- 如果混入对象没有钩子函数,返回目标组件的狗子函数组成的数组
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
/*
1. 目标组件没有这个钩子函数,直接使用混入的钩子函数
2. 目标组件有钩子函数
2.1 如果混入对象也该钩子函数,则混入对象的钩子函数拼接目标组件的钩子函数为一个数组
2.2 如果混入对象没有钩子函数,返回目标组件的狗子函数组成的数组
*/
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
})
值为对象的选项
值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
源码分析
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
// shared/utils.js
// 将原对象属性混入到目标对象中
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}