欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

vue核心面试题:Vue中相同逻辑如何抽离?

程序员文章站 2022-03-22 09:44:51
一、mixin使用vue中的Vue.mixin,给组件每个生命周期、函数等混入一些公共的逻辑。mixin可以放在全局使用,也可以放在组件中使用。vue实例化选项mixins ,extend将在init函数中通过mergeOptions合并至实例属性$options,他们的区别是extend为options对象,mixins为options数组,同时extend的方法将比mixins先执行,但他们都会在Vue.extend 与Vue.mixin之后执行。mixins:[]和extend:......

一、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