当前位置: 首页 > 工具软件 > mixin > 使用案例 >

Vue 源码之 mixin 原理

安星汉
2023-12-01

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选项;
  • propsinjectdirectives进行规范化处理;
  • 判断混入对象是否有 mixinsextends选项,有则递归进行合并;
  • 定义一个 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属性的合并主要是 mergeDataOrFnmergeData

  • 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
    }
}

componentsdirectivesfilters的合并是先拷贝一份原先的属性对象,然后对拷贝的对象进行扩展,它会遍历传入的对象,将传入对象的属性赋值给拷贝对象,如果有重复的,则后者覆盖前者;

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

propsmethodsinjectcomputed的合并都是先定义一个对象 ret,先遍历前者的属性或方法,对 ret进行扩展,如果后者有相应的propsmethodsinjectcomputed等属性,则将后者的覆盖前者的属性或方法;

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进行扩展,再遍历后者的属性或方法,后者将覆盖前者的属性或方法;
  • 默认策略:策略中没有定义的策略,后者有则返回后者,否则返回前者;
 类似资料: