vue中的mixin

148 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情

高频面试题:vue中的mixin?

一、全局mixin

Vue.mixin({
  created: function () {
    console.log('from-mixin-name: ', this.name);
  },
  data() {
    return {
      name: 'mixin-name'
    }
  },
  methods: {
    sayHello() {
      console.log('hello mixin!')
    }
  },
})

const A = {
  template: `<div @click="sayHello">组件A中的name为: {{name}}</div>`,
  data() {
    return {
      name: 'component-a'
    }
  },
  created() {
    console.log('from-A-name: ', this.name);
  },
  methods: {
    sayHello() {
      console.log('hello A!')
    }
  },
}

const B = {
  template: `<div @click="sayHello">组件B中的name为: {{name}}</div>`,
}

new Vue({
  el: '#app',
  template: `<div>
    <div>组件Parent中的name为: {{name}}</div>
    <A></A>
    <B></B>
  </div>`,
  components: {
    A,
    B
  },
  data() {
    return {
      name: 'parent'
    }
  },
})

在当前例子中先通过Vue.mixin的方式混入createddatamethods属性。在组件A中有定义自己的createddatamethods,而组件B中则没有,下面探索vue对于混入的合并策略。

1、Vue.mixin的定义

initGlobalAPI(Vue)阶段通过initMixin(Vue)进行定义:

function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
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)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  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)
      }
    }
  }

  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
}

执行Vue.mixin的时候将当前this.options和混入的mixin通过mergeOptions进行合并。

在当前例子中,首先定义options的空对象,然后通过遍历将父组件和子组件中的key通过mergeField函数在strats中选择预先定义好的合并策略进行合并,如果未定义则采用默认合并策略defaultStrat

针对datamethodscreatedwatchprops等不同的属性有不同的合并策略,如下图所示,例子中只对datamethodscreated进行演示。

image.png

2、合并策略

Vue.mixin的时候会执行一次mergeOptions,在实例化Vue的时候会执行到mergeOptions逻辑,在渲染组件A和组件B的过程中也会执行到合并策略mergeOptions

(1)data合并策略

/**
 * Helper that recursively merges two data objects together.
 */
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]
    // in case the object is already observed...
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

/**
 * Data
 */
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)
}

data的合并策略需要执行mergeDataOrFnVue.mixin时未传入vm,并且parentVal也不存在,所以直接返回childVal

在执行new Vue的时候会执行到this._init中的合并逻辑mergeDataOrFn时,此时传入了vmparentValchildVal都存在,会执行到逻辑mergedInstanceDataFn。若childVal为函数,并且执行能够获得instanceData,就会执行到mergeData(instanceData, defaultData)逻辑。

mergeData会通过for循环和递归让对象树的枝叶也实现合并,如下:

Vue.mixin({
  data() {
    return {
      baseData: {
        a: {
          c: 2
        },
        b: 3
      }
    }
  },
})

new Vue({
  el: '#app',
  template: `<div>{{baseData}}</div>`,
  data() {
    return {
      baseData: {
        a: {
          c: 1
        },
      }
    }
  }
})

最终的执行结果为:

baseData:{
    a: {
        c: 1
    },
    b: 3
}

对于baseData中的a属性因为组件中也有,采用了组件中的a:{c:1},对于属性b组件中没有,则采用了混入的b: 3

(2)created合并策略

/**
 * Hooks and props are merged as arrays.
 */
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
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

生命周期的合并策略是首先执行混入的声明周期,再执行组件的生命周期。

(3)methods合并策略

/**
 * Mix properties into target object.
 */
function extend(to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key];
  }
  return to;
}
/**
 * Other object hashes.
 */
strats.methods = 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
}

方法methods的合并策略是遍历childVal中的键值,用来覆盖parentVal中存在的键值,并且新增parentVal中不存在的键值。

通过以上的合并策略,在Vueoptions中就有了datacreatedmethods了。

image.png

Vue项目是由组件像堆积木一样堆起来的,在创建组件构造函数的时候,会执行到Vue.extend方法, 其中有合并逻辑:

Sub.options = mergeOptions(
  Super.options,
  extendOptions
);

通过合并,Vue.mixin混入的datacreatedmethods就会和当前组件中的extendOptions一起合并成为子组件的options,在后面所有的子组件成为构造函数的时候全局混入的选项都会被通过合适的合并策略合并进来。所以,一般情况下是不建议使用全局混入的。

二、局部mixin

const mixin = {
  created: function () {
    console.log('from-mixin-name: ', this.name);
  },
  data() {
    return {
      name: 'mixin-name'
    }
  },
  methods: {
    sayHello() {
      console.log('hello mixin!')
    }
  },
}

const A = {
  template: `<div @click="sayHello">组件A中的name为: {{name}}</div>`,
  mixins: [mixin],
  data() {
    return {
      name: 'component-a'
    }
  },
  created() {
    console.log('from-A-name: ', this.name);
  },
  methods: {
    sayHello() {
      console.log('hello A!')
    }
  },
}

const B = {
  template: `<div>组件B</div>`,
}

new Vue({
  el: '#app',
  template: `<div><div>组件Parent</div><A></A><B></B></div>`,
  components: {
    A,
    B
  },
  data() {
    return {
      name: 'parent'
    }
  },
})

在当前例子中先定义了包含createddatamethods属性的mixin。在组件A中有定义自己的createddatamethods,然后又配置了选项mixins: [mixin],在子组件执行合并逻辑mergeOptions时有逻辑:

if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
    }
}

也就是说如果当前组件中存在mixins时,会通过for循环递归调用mergeOptions,以达到将mixins进行混入的目的。然后,再将和子组件中的options进行合并。这种混入方式只会影响到A组件,所以显得更为灵活。

总结

全局混入和局部混入视情况而定,主要区别在全局混入是通过Vue.mixin的方式将选项混入到了Vue.options中,在所有获取子组件构建函数的时候都将其进行了合并,而局部混入是通过循环和递归的方式将选项通过配置mixins选项的方式合并到当前的子组件中。