Vue-源码详解mixin混入和合并策略

在vue里面,混入(mixin)是一种特殊的使用方式。一个混入对象可以包含任意的组件选项,可根据需求随意“封装”组件的可复用单元,并在使用时根据一定的策略合并到组件的选项当中,使用时与组件自身选项无异。
官方文档对mixin介绍比较少,不能了解甚少,于是便想研究下源码对它混入做个研究和总结

本文基于Vue源码2.x版本

一、说在前面

在分析mixin之前,先看看两个方法,它们在混入的合并过程中扮演着重要的角色
(1) Vue.extend()
Vue.extend()是基础Vue构造器,参数是一个包含组件选项的对象,可用于显式的扩展组件和混入对象,如

var Component = Vue.extend({
  mixins: [myMixin]
})

其实,在组件内部的混入合并也是通过它来完成的
(2) extend()
extend()是对象合并方法,参数是源和目标两个对象,用于对象的合并操作

extend(target, source)

其中source对象将合并到target对象,如果source和target的key值相同,则直接覆盖,否则属性添加;作用类似于Object.assgin()和underscore的_.extend(destination, *sources)

二、合并图示

通过对vue源码的研究,我发现混入对于选项的“合并”并不是一步到位的,而是两两合并,并通过合并策略和优先级向一定的方向逐步进行合并操作,最终才得到合并的结果,就像默认的选项合并策略:

const defaultStrat = function (parentVal: any, childVal: any): any {
    return childVal === undefined
        ? parentVal
        : childVal
}

其中childVal和parentVal是每一次合并中的两个源和目标对象,比如:vm.options.xx和mixin.options.xx,下面用一个例子和图示进行说明
示例代码:

import Vue from 'vue'

Vue.mixin({
  data () {
    return {
      msg: '全局混入-msg',
      msg1: '全局混入1-msg-1',
      msg2: '全局混入1-msg-2',
      msg4: '全局混入1-msg4',
      msg5: '全局混入1-msg5',
      site: {
        name: '腾讯',
        url: 'www.tenant.com',
        hahaList: [3, 4],
        city: 'shenzhen'
      }
    }
  },
  created: function () {
    console.log('全局混入1 --- created')
  },
  methods: {
    startMix: function () {
      console.log('全局混入1 --- startMix')
    }
  },
  computed: {
    getMsg: function () {
      return 'getMsg 全局混入1!!'
    },
    getMsg2: function () {
      return 'getMsg2 全局混入1!!'
    },
    getMsg5: function () {
      return 'getMsg5 全局混入1!!'
    }
  }
})

Vue.mixin({
  data () {
    return {
      msg: '全局混入2-msg',
      msg1: '全局混入2-msg-1',
      msg4: '全局混入2-msg4',
      numList: [4, 5],
      site: {
        name: 'alibaba',
        hahaList: [3, 4],
        url: 'www.alibaba.com',
        people: 30000
      }
    }
  },
  created: function () {
    console.log('全局混入2 --- created')
  },
  methods: {
    startMix: function () {
      console.log('全局混入2 --- startMix')
    },
    hello: function () {
      console.log('全局混入2 --- hello')
    }
  },
  computed: {
    getMsg: function () {
      return 'getMsg 全局混入2!!'
    },
    getMsg2: function () {
      return 'getMsg2 全局混入2!!'
    }
  }
})

var localMix3 = {
  data () {
    return {
      msg: '实例混入3',
      msg1: '实例混入3-msg1',
      msg2: '实例混入3-msg2',
      msg3: '实例混入3-msg3',
      site: {
        name: '淘宝111',
        url: 'www',
        country: 'China'
      }
    }
  },
  created: function () {
    console.log('实例混入3 --- created')
  },
  methods: {
    startMix: function () {
      console.log('实例混入3 --- startMix')
    },
    hello: function () {
      console.log('实例混入3 --- hello')
    }
  },
  computed: {
    getMsg: function () {
      return 'getMsg 实例混入3!!'
    },
    getMsg2: function () {
      return 'getMsg2 实例混入3!!'
    },
    getMsg3: function () {
      return 'getMsg3 实例混入3!!'
    },
    getMsg4: function () {
      return 'getMsg4 实例混入3!!'
    }
  }
}
var localMix2 = {
  data () {
    return {
      msg: '实例混入2',
      msg1: '实例混入2-msg1',
      msg2: '实例混入2-msg2',
      site: {
        name: '淘宝',
        url: 'www.taobao.com',
        title: 'I am 淘宝'
      }
    }
  },
  created: function () {
    console.log('实例混入2 --- created')
  },
  methods: {
    startMix: function () {
      console.log('实例混入2 --- startMix')
    },
    hello: function () {
      console.log('实例混入2 --- hello')
    }
  },
  computed: {
    getMsg: function () {
      return 'getMsg 实例混入2!!'
    },
    getMsg2: function () {
      return 'getMsg2 实例混入2!!'
    },
    getMsg3: function () {
      return 'getMsg3 实例混入2!!'
    }
  }
}
var localMix1 = {
  data () {
    return {
      msg: '实例混入1',
      msg1: '实例混入1-msg1',
      numList: [2, 3],
      site: {
        name: '淘宝',
        url: 'www.taobao.com',
        hahaList: [3, 4]
      }
    }
  },
  mixins: [localMix2],
  created: function () {
    console.log('实例混入1 --- created')
  },
  methods: {
    startMix: function () {
      console.log('实例混入1 --- startMix')
    },
    hello: function () {
      console.log('实例混入1 --- hello')
    }
  },
  filters: {
    capitalize2: function (value) {
      if (!value) return ''
      return value + ' 实例混入1!!'
    }
  },
  computed: {
    getMsg: function () {
      return 'getMsg 实例混入1!!'
    },
    getMsg2: function () {
      return 'getMsg2 实例混入1!!'
    }
  }
}

export default {
  name: 'basicExtend',
  data () {
    return {
      msg: 'basicExtend',
      numList: [1, 2],
      site: {
        name: '百度',
        hahaList: [1, 2]
      }
    }
  },
  mixins: [localMix3, localMix1],
  created: function () {
    console.log('basicExtend --- created')
  },
  methods: {
    startMix: function () {
      console.log('basicExtend --- startMix')
    }
  },
  computed: {
    showData: function () {
      return JSON.stringify(this.$data)
    },
    getMsg: function () {
      return this.msg + ' !!!'
    }
  }
}

其中合并后的data和输出如下,

vm.$data:
{
    "msg":"basicExtend",
    "numList":[1,2],
    "site":{
        "name":"百度",
        "hahaList":[1,2],
        "url":"www.taobao.com",
        "title":"I am 淘宝",
        "country":"China",
        "people":30000,
        "city":"shenzhen"
    },
    "msg1":"实例混入1-msg1",
    "msg2":"实例混入2-msg2",
    "msg3":"实例混入3-msg3",
    "msg4":"全局混入2-msg4",
    "msg5":"全局混入1-msg5"
}

computed计算属性:
getMsg: basicExtend !!!
getMsg2: getMsg2 实例混入1!!
getMsg3: getMsg3 实例混入2!!
getMsg4: getMsg4 实例混入3!!
getMsg5: getMsg5 全局混入1!!

下面根据这个例子整理的“合并父子图示”,根据后面的讲解再反过来看为什么合并的结果会是这样。

vue-mixins.png

图示描述了源代码中选项合并方法的入参"父子"关系,其中,

  • 全局注册的混入最先完成混入,并按注册的顺序来逐个合并,先注册的先完成混入合并,依次类推
  • 局部注册的混入次之,并按mixins数组里声明的顺序依次完成合并
  • 每个混入也可以包含mixins局部混入数组,mixins先完成合并,本混入的options再进行合并
  • 组件options最后完成混入合并
  • 先合并的"优先级"低,后合并的"优先级"高,也就是组件的options合并优先级最高
  • 不同的选项根据自身的混入策略合并方向不一样,这个在下面会有说

三、选项合并

混入对象的混入是每个对象选项和组件选项的“混入合并”,比如:options.data、options.props等,根据不同选项合并策略的不同,各类选项会以不同的方式和方向进行“合并”操作
(1) el,propsData

合并方向: parent --> child

源码:

 src/core/util/options.js

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)
  }
}
/**
 * Default strategy.
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

可见,el , propsData使用的是默认合并策略,默认策略比较简单干脆,以child选项为主,若无则使用parent选项

(2) 生命周期钩子

合并方向: parent --> child

源码:

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

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
}

可见,LIFECYCLE_HOOKS会将每个hook合并成一个数组,按照图示从父到子开始一步步链接合并成数组,parent在前,child在后。在钩子触发时,按照数组从头顺序调用触发,所以我们看到调用顺序是这样的,与图示一致:

全局混入hook --> 实例混入hook ... --> 组件实例hook

(3) data

合并方向: parent --> child

源码:

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

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 () {
      // parentVal作为from,childVal作为to,parent->child方向合并
      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) {
        // parentVal作为from,childVal作为to,parent->child方向合并
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

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]  // child
    fromVal = from[key] // parent
    if (!hasOwn(to, key)) {
      set(to, key, fromVal) //若to没有此key,添加它
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      //若to有此key,且不等
      //若to有此key,值非对象,否则进行深度合并
      mergeData(toVal, fromVal)
    }
  }
  return to
}

这里set()方法操作添加key并创建响应属性值
可以看到,data的合并比较复杂,按照图示从父到子开始递归合并,以child为主,比较key规则如下:

  • 若child无此key,parent有,直接合并此key
  • 若child和parent都有此key,且非object类型,忽略不作为
  • 若child和parent都有此key,且为object类型,则递归合并对象

(4) components,directives,filters

合并方向: 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
  }
}

可以看到,components, directives,filters的合并策略比较简单,使用extend方法合并为一个对象,按
照图示从子到父进行合并。
其实这是采用原型链委托的方式在合并时把child的属性委托在parent上,这样在使用的时候,在child上查找,没有的再从parent上找,以此类推,所以child的优先级的更高的。

(5) watch

合并方向: parent --> child

源码:

strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  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) //获取parent选项
  for (const key in childVal) {
    let parent = ret[key] //获取parent选项值
    const child = childVal[key] //获取child选项值
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    //每个wather选项合并为数组
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}

可见,watch会将每个watcher合并成一个数组,按照图示从父到子顺序合并。在同名wather属性触发时,按照数组从头顺序调用触发,所以我们看到触发顺序是这样的,与图示一致:

全局混入 --> 实例混入 ... --> 组件实例

(6) props,methods,computed,inject

合并方向: parent <-- 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) //合并parent
  if (childVal) extend(ret, childVal) //合并child
  return ret
}

可以看到,props,methods,computed,inject的合并策略和components比较相似,都是使用extend方法合并为一个对象,按照图示从子到父进行合并,所以在调用查找时child优先级更高。

(7) provide

合并方向: parent --> child

源码:

strats.provide = mergeDataOrFn

provide的合并策略和data类似

选项合并策略

如上,对于mixin和组件的每个选项都有对应的合并策略,你可以像下面这样改变默认的合并策略

const strats = Vue.config.optionMergeStrategies
strats.methods = strats.data

但是对于默认选项不建议这么做,会引起合并错误,虽然提供了手段
对于新增的选项,比如vuex,myVOption,也需要什么合并策略,可以使用现有的策略,比如:

Vue.config.optionMergeStrategies.myVOption = strats.methods

也可以自定义策略

Vue.config.optionMergeStrategies.myVOption = function(
  parentVal: ?Object,
  childVal: ?Object,
): ?Object {
    if (!parentVal) return childVal
    if (!childVal) return parentVal
    return extend(parentVal, childVal)
}

注意:全局混入将对所有的vue实例有效,尽量不要使用;但可以用于插件的发布,比如发布一个画图插件,并提供若干绘画方法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容