vue mixins(混入)遇到的坑以及原理分析

背景

mixins的一般用法

就我自己个人而言,mixins一般用的比较多的就是定义一个混入对象,然后在组件里或者新建的vue对象里使用mixins,用法简单明了。基本上和官网用法一样,这里以官网示例为例:

// 定义一个混入对象
var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}

// 定义一个使用混入对象的组件
var Component = Vue.extend({
  mixins: [myMixin]
})

var component = new Component() // => "hello from mixin!"
VM1740:8 hello from mixin!

全局使用mixins

直到最近想写一个通用业务(埋点),希望能在公共文件引入,不需要改动其他文件的前提下,比如统计进入页面、离开页面的时间,各个生命周期的时间等等。很不凑巧,我用了全局mixins,才发现原来所有的vue实例都会被统计到,不仅仅是当前页面的vue实例,而是当前页面所有用到的子组件都会被统计到。而全局mixins使用的警告,官网早已经告知过,果然什么都要自己试试才会印象深刻,之前也看到过这段文字警告:

也可以全局注册混入对象。
但是注意使用!
 一旦使用全局混入对象,将会影响到 所有 之后创建的 Vue 实例。
使用恰当时,则可以为自定义对象注入处理逻辑。

还是古人有智慧,“纸上得来终觉浅,绝知此事要躬行”!

mixin原理

这就引起了我的好奇,全局的mixins为什么会影响到所有的子组件?为此特地下载了最新的vue 源码,2.6.8版本,查看了下源码,才发现有部分语法没见过,原来是flow的语法检查,嗯,很抱歉用了这么久的2.x 版本的vue竟然不知道这个。
mixins文件代码很少,如下:

import { mergeOptions } from '../util/index'

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

可以看出,其实就是把当前Vue实例的options和传入的mixin合并,再返回。真正的实现是靠mergeOptions函数实现的。

mergeOptions函数实现

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) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      //处理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) {
    //这里parent是this.options
    mergeField(key)
  }
  for (key in child) {
    //这里child是mixins,同时检查parent中是否已经有key即是否合并过
    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
}

上面的注释已经标明,首先我们要明确这个函数传进去的两个参数分别是this.options 和 mixin,而mergeOptions函数则实现了递归遍历this.options,然后执行mergeField,返回最终合并的this.options。到这里基本上就是mixin的所有实现了。但是mergeField函数看似简单,实际上是很重要的,我还是很好奇这个函数的实现的。

mergeField函数实现

const strats = config.optionMergeStrategies
//默认的合并策略
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}
function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }

于是我又在代码里找了找,发现strats这个对象有很多属性,如下:

strats.el = strats.propsData = function (parent, child, vm, key) {
strats.data = function (
strats.watch = function (
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
.....
.....

这个时候就很清晰了,一般我们执行mergeField 里的key基本上就是上面strats的属性了,用的最多的可能就是data、methods、props了。所以如果我们在mixins中用到了data,其本质上就是合并当前vue实例对象里的data和我们传进去的mixin里的data,其他属性也是一样的,只是合并策略还需深入研究。

不得不感叹,vue官网真的没有任何废话,官网其实早已给出了选项合并策略。

  • 当组件和混入对象含有同名选项时,这些选项将以恰当的方式混合。比如,数据对象在内部会进行递归合并,在和组件的数据发生冲突时以组件数据优先。
  • 值为对象的选项,例如 methods, components 和 directives,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。

而回到代码层面上,如果是key为data的话,代码实现如下:

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

如果key是 methods, components 和 directives

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
}

组件局部注册的原理

其实到这里,我依然不知道为什么全局mixins会影响到所有的子组件,直到我发现了这段代码:

function initAssetRegisters (Vue) {
    /**
     * Create asset registration methods.
     */
    ASSET_TYPES.forEach(function (type) {
      Vue[type] = function (
        id,
        definition
      ) {
        if (!definition) {
          return this.options[type + 's'][id]
        } else {
          /* istanbul ignore if */
          if (type === 'component') {
            validateComponentName(id);
          }
          if (type === 'component' && isPlainObject(definition)) {
            definition.name = definition.name || id;
            definition = this.options._base.extend(definition);
          }
          if (type === 'directive' && typeof definition === 'function') {
            definition = { bind: definition, update: definition };
          }
          this.options[type + 's'][id] = definition;
          return definition
        }
      };
    });
  }

直到看到这段代码,终于明白了为何会影响所有子组件了,局部注册组件,本质上其实是Vue.extends().而extend里面最终也会执行mergeOptions()函数。至此,终于明白了全局mixins的影响以及实现原理。不过,extend就不再在这里多聊了,等我下次把extend看明白了,再总结吧。

你可能感兴趣的:(vue mixins(混入)遇到的坑以及原理分析)