vue核心面试题:Vue中相同逻辑如何抽离?
一、mixin
使用vue中的Vue.mixin,给组件每个生命周期、函数等混入一些公共的逻辑,另外混入对象的钩子将在组件自身钩子之前调用。。mixin可以放在全局使用,也可以放在组件中使用。
vue实例化选项mixins , extend将在init函数中通过mergeOptions合并至实例属性$options,他们的区别是extend为options对象,mixins为options数组,同时 extend的方法将比mixins先执行,但他们都会在 Vue.extend 与 Vue.mixin之后 执行。mixins:[]和extend:{}使用在组件中,Vue.extend 与 Vue.mixin使用在全局。
- 全局注册的混入最先完成混入,并按注册的顺序来逐个合并,先注册的先完成混入合并,依次类推
- 局部注册的混入次之,并按mixins数组里声明的顺序依次完成合并
- 先合并的"优先级"低,后合并的"优先级"高,也就是组件的options合并优先级最高
在vue3.0中将mixin改为了Composition API,因为mixin不知道这些数据的来源,会很乱。
二、源码
在vue初始化的时候,会调initMixin方法,在initMixin中主要就是调用了mergeOptions方法进行合并然后将合并之后的赋值给Vue构造函数的options属性上,mergeOptions方法会帮我们合并数据、方法、生命周期等,在mergeOptions中首先会判断组件中组件中是否有extends和mixins,如果有深度递归再次调用mergeOptions方法,最后调用mergeField 方法,mergeField 方法中会调用不同属性的合并策略进行合并。
1.initMixin ,文件位置:src/core/global-api/mixin.js
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
//mergeOption,将Vue构造函数的Options属性与传入的mixin参数进行合并
//合并之后再赋值给Vue构造函数的Options属性
this.options = mergeOptions(this.options, mixin)
return this
}
// mergeOptions帮我们合并数据、方法、生命周期
}
2.mergeOptions,文件位置:src/core/global-api/mixin.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
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
if (!child._base) {
// 如果是组件中使用的,会在组件中找看是否有extends,如果有递归合并extends
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
// 如果是组件中使用的,会在组件中找看是否有mixins,如果有递归合并mixin
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
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)
}
return options
}
3.合并策略
(1) el,propsData合并
el , propsData使用的是默认合并策略,默认策略比较简单干脆,如果以child有就以child选项为主,若无则使用parent选项。
源码:
const strats = config.optionMergeStrategies
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)
}
}
默认合并策略
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
(2) 生命周期钩子
在init的时候执行callHook时,会循环数组,让数组依次执行,最终合并的是一个数组。会循环LIFECYCLE_HOOKS 中的每一个,然后调取mergeHook,mergeHook就是去看当前child有没有,如果有就看parent有没,如果两个都有,就把chiid和parent合并起来,如果有child没有parent,那么就看child是不是一个数组,如果是数组就直接返回,如果不是数组就把child包装成一个数组,如果没有child就直接返回parent。
总结:LIFECYCLE_HOOKS会将每个hook合并成一个数组,全局混入hook --> 实例混入hook ... --> 组件实例hook从父到子开始一步步链接合并成数组,parent在前,child在后。
export function callHook (vm: Component, hook: string) {
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
// 循环数组,让数组依次执行,最终合并的是一个数组
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
// LIFECYCLE_HOOKS数组
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
]
// 循环LIFECYCLE_HOOKS,并调用mergeHook
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
// 对child和parent进行合并
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
}
(3)data
data的合并是从父到子开始递归合并,以child为主,比较key:
- 如果child无此key,parent有,直接合并此key
- 若child和parent都有此key,且为object类型,则递归深度合并对象
- 若child和parent都有此key,且非object类型,忽略不作为
// 文件地址:src/core/util/options.js
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)
}
// 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
}
//当父和子都有时,我们需要返回一个函数,该函数返回两个函数合并的结果,不需要检查parentVal是否为函数,因为它必须是一个函数来传递以前的合并。
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) {
// 将parent合并到child中
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
// 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]
// 如果key是__ob__就继续
if (key === '__ob__') continue
toVal = to[key] // child
fromVal = from[key] // parent
if (!hasOwn(to, key)) {
// 这里set()方法操作添加key并创建响应属性值
set(to, key, fromVal) // 若child没有此key,就将它添加到child
} else if (
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
// 若child有此key,并且不相等并且值是对象就进行深度合并
mergeData(toVal, fromVal)
}
}
return to
}
(4)components,directives,filters
components, directives,filters的合并策略使用extend方法合并为一个对象,从子到父进行合并。这是采用原型链委托的方式在合并时把child的属性委托在parent上,这样在使用的时候,在child上查找,没有的再从parent上找,以此类推,所以child的优先级的更高的。
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
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) // child合并到parent
} else {
return res
}
}
(5)watch
watch会将每个watcher合并成一个数组,全局混入 --> 实例混入 --> 组件实例从父到子顺序合并。在同名wather属性触发时,按照数组从头顺序调用触发。
strats.watch = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
// 使用Firefox的Object.prototype.watch
if (parentVal === nativeWatch) parentVal = undefined
if (childVal === nativeWatch) childVal = undefined
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
}
(6) props,methods,computed,inject
props,methods,computed,inject的合并策略和components比较相似,都是使用extend方法合并为一个对象,从子到父进行合并,所以在调用查找时child优先级更高。
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) // 使用extend合并parent
if (childVal) extend(ret, childVal) // 使用extend合并child
return ret
}
(7)provide
provide 的合并策略一样都是调用mergeOptions进行合并
strats.provide = mergeDataOrFn
只要创建一个子类,就会自动一个mixin方法,在extend.js中就会进行合并,这是子类默认有的,如果要是合并就会去调mixin。
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
三、mixin的使用
1.全局使用:
一旦使用全局混入,它将影响每一个之后创建的 Vue 实例。
Vue.mixin({
data () {},
methods: {},
computed: {},
mounted () {},
created () {}
...
})
2.局部混入使用
var myMixin = {
data () {},
methods: {},
computed: {},
mounted () {},
created () {}
...
}
export default {
mixins: [myMixin]
}
四、自定义选项合并策略
自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向 Vue.config.optionMergeStrategies
添加一个函数:
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合并后的值
}
对于多数值为对象的选项,可以使用与 methods
相同的合并策略:
var strategies = Vue.config.optionMergeStrategies
strategies.myOption = strategies.methods
vuex 1.x 的混入策略里找到一个更高级的例子:
const merge = Vue.config.optionMergeStrategies.computed
Vue.config.optionMergeStrategies.vuex = function (toVal, fromVal) {
if (!toVal) return fromVal
if (!fromVal) return toVal
return {
getters: merge(toVal.getters, fromVal.getters),
state: merge(toVal.state, fromVal.state),
actions: merge(toVal.actions, fromVal.actions)
}
}
本文地址:https://blog.csdn.net/qq_42072086/article/details/109483039