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

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值