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

背景

mixins的一般用法

就我自己個(gè)人而言,mixins一般用的比較多的就是定義一個(gè)混入對(duì)象,然后在組件里或者新建的vue對(duì)象里使用mixins,用法簡(jiǎn)單明了?;旧虾凸倬W(wǎng)用法一樣,這里以官網(wǎng)示例為例:

// 定義一個(gè)混入對(duì)象
var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}

// 定義一個(gè)使用混入對(duì)象的組件
var Component = Vue.extend({
  mixins: [myMixin]
})

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

全局使用mixins

直到最近想寫一個(gè)通用業(yè)務(wù)(埋點(diǎn)),希望能在公共文件引入,不需要改動(dòng)其他文件的前提下,比如統(tǒng)計(jì)進(jìn)入頁(yè)面、離開(kāi)頁(yè)面的時(shí)間,各個(gè)生命周期的時(shí)間等等。很不湊巧,我用了全局mixins,才發(fā)現(xiàn)原來(lái)所有的vue實(shí)例都會(huì)被統(tǒng)計(jì)到,不僅僅是當(dāng)前頁(yè)面的vue實(shí)例,而是當(dāng)前頁(yè)面所有用到的子組件都會(huì)被統(tǒng)計(jì)到。而全局mixins使用的警告,官網(wǎng)早已經(jīng)告知過(guò),果然什么都要自己試試才會(huì)印象深刻,之前也看到過(guò)這段文字警告:

也可以全局注冊(cè)混入對(duì)象。
但是注意使用!
 一旦使用全局混入對(duì)象,將會(huì)影響到 所有 之后創(chuàng)建的 Vue 實(shí)例。
使用恰當(dāng)時(shí),則可以為自定義對(duì)象注入處理邏輯。

還是古人有智慧,“紙上得來(lái)終覺(jué)淺,絕知此事要躬行”!

mixin原理

這就引起了我的好奇,全局的mixins為什么會(huì)影響到所有的子組件?為此特地下載了最新的vue 源碼,2.6.8版本,查看了下源碼,才發(fā)現(xiàn)有部分語(yǔ)法沒(méi)見(jiàn)過(guò),原來(lái)是flow的語(yǔ)法檢查,嗯,很抱歉用了這么久的2.x 版本的vue竟然不知道這個(gè)。
mixins文件代碼很少,如下:

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

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

可以看出,其實(shí)就是把當(dāng)前Vue實(shí)例的options和傳入的mixin合并,再返回。真正的實(shí)現(xiàn)是靠mergeOptions函數(shù)實(shí)現(xiàn)的。

mergeOptions函數(shù)實(shí)現(xiàn)

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)
  //上面的代碼可以略過(guò)不看,主要是檢查各種格式的
  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,同時(shí)檢查parent中是否已經(jīng)有key即是否合并過(guò)
    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
}

上面的注釋已經(jīng)標(biāo)明,首先我們要明確這個(gè)函數(shù)傳進(jìn)去的兩個(gè)參數(shù)分別是this.options 和 mixin,而mergeOptions函數(shù)則實(shí)現(xiàn)了遞歸遍歷this.options,然后執(zhí)行mergeField,返回最終合并的this.options。到這里基本上就是mixin的所有實(shí)現(xiàn)了。但是mergeField函數(shù)看似簡(jiǎn)單,實(shí)際上是很重要的,我還是很好奇這個(gè)函數(shù)的實(shí)現(xiàn)的。

mergeField函數(shù)實(shí)現(xiàn)

const strats = config.optionMergeStrategies
//默認(rèn)的合并策略
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)
  }

于是我又在代碼里找了找,發(fā)現(xiàn)strats這個(gè)對(duì)象有很多屬性,如下:

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

這個(gè)時(shí)候就很清晰了,一般我們執(zhí)行mergeField 里的key基本上就是上面strats的屬性了,用的最多的可能就是data、methods、props了。所以如果我們?cè)趍ixins中用到了data,其本質(zhì)上就是合并當(dāng)前vue實(shí)例對(duì)象里的data和我們傳進(jìn)去的mixin里的data,其他屬性也是一樣的,只是合并策略還需深入研究。

不得不感嘆,vue官網(wǎng)真的沒(méi)有任何廢話,官網(wǎng)其實(shí)早已給出了選項(xiàng)合并策略。

  • 當(dāng)組件和混入對(duì)象含有同名選項(xiàng)時(shí),這些選項(xiàng)將以恰當(dāng)?shù)姆绞交旌稀1热?,?shù)據(jù)對(duì)象在內(nèi)部會(huì)進(jìn)行遞歸合并,在和組件的數(shù)據(jù)發(fā)生沖突時(shí)以組件數(shù)據(jù)優(yōu)先。
  • 值為對(duì)象的選項(xiàng),例如 methods, components 和 directives,將被混合為同一個(gè)對(duì)象。兩個(gè)對(duì)象鍵名沖突時(shí),取組件對(duì)象的鍵值對(duì)。

而回到代碼層面上,如果是key為data的話,代碼實(shí)現(xiàn)如下:

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
}

組件局部注冊(cè)的原理

其實(shí)到這里,我依然不知道為什么全局mixins會(huì)影響到所有的子組件,直到我發(fā)現(xiàn)了這段代碼:

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

直到看到這段代碼,終于明白了為何會(huì)影響所有子組件了,局部注冊(cè)組件,本質(zhì)上其實(shí)是Vue.extends().而extend里面最終也會(huì)執(zhí)行mergeOptions()函數(shù)。至此,終于明白了全局mixins的影響以及實(shí)現(xiàn)原理。不過(guò),extend就不再在這里多聊了,等我下次把extend看明白了,再總結(jié)吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容