Vue - 深入浅出 Mixin

什么 mixin

mixin, 意为混入。

比如去买冰激凌,我先要一点奶油的,再来点香草的。我就可以吃一个奶油香草的冰激凌。如果再加点草莓,我可以同时吃三个口味的冰激凌。

代码表示

假设把你已有的奶油味的称为 base,把要添加的味道称为 mixins。用 js 伪代码可以这么来写:

1const base = { 2  hasCreamFlavor() { 3    return true; 4  } 5} 6const mixins = { 7  hasVanillaFlavor() { 8    return true; 9  }, 10  hasStrawberryFlavor() { 11    return true; 12 } 13} 14 15function mergeStrategies(base, mixins) { 16  return Object.assign({}, base, mixins); 17} 18// newBase 就拥有了三种口味。 19const newBase = mergeStrategies(base, mixins); 20

注意一下这个 mergeStrategies

合并策略可以你想要的形式,也就是说你可以自定义自己的策略,这是其一。另外要解决冲突的问题。上面是通过 Object.assign 来实现的,那么 mixins 内的方法会覆盖base 内的内容。如果这不是你期望的结果,可以调换 mixin 和 base 的位置。

组合大于继承 && DRY

想象一下上面的例子用继承如何实现?由于 js 是单继承语言,只能一层层继承。写起来很繁琐。这里就体现了 mixin 的好处。符合组合大于继承的原则。

mixin 内通常是提取了公用功能的代码。而不是每一个地方都写一遍。符合 DRY 原则。

什么是 vue mixin

vue mixin 是针对组件间功能共享来做的。可以对组件的任意部分(生命周期, data等)进行mixin,但不同的 mixin 之后的合并策略不同。在源码分析部分会介绍细节。

组件级 mixin

假设两个功能组件 model 和 tooltip ,他们都有一个显示和关闭的 toggle 动作:

1//modal 2const Modal = { 3  template: '#modal', 4  data() { 5    return { 6      isShowing: false 7    } 8  }, 9  methods: { 10    toggleShow() { 11      this.isShowing = !this.isShowing; 12    } 13  } 14} 15 16//tooltip 17const Tooltip = { 18  template: '#tooltip', 19  data() { 20    return { 21      isShowing: false 22    } 23  }, 24  methods: { 25    toggleShow() { 26      this.isShowing = !this.isShowing; 27    } 28  } 29} 30

可以用 mixin 这么写:

1const toggleMixin = { 2  data() { 3    return { 4      isShowing: false 5    } 6  }, 7  methods: { 8    toggleShow() { 9      this.isShowing = !this.isShowing; 10    } 11  } 12} 13 14const Modal = { 15  template: '#modal', 16  mixins: [toggleMixin] 17}; 18 19const Tooltip = { 20  template: '#tooltip', 21  mixins: [toggleMixin], 22}; 23

全局 mixin

全局 mixin 会作用到每一个 vue 实例上。所以使用的时候要慎重。通常会用 plugin 来显示的声明用到了那些 mixin。

比如 vuex。我们都知道它在每一个实例上扩展了一个 在任意一个组件内可以调用store。那么他是如何实现的呢?

src/mixin.js

1export default function (Vue) { 2  const version = Number(Vue.version.split('.')[0]) 3 4  if (version >= 2) { 5    Vue.mixin({ beforeCreate: vuexInit }) 6  } else { 7    // override init and inject vuex init procedure 8    // for 1.x backwards compatibility. 9    const _init = Vue.prototype._init 10    Vue.prototype._init = function (options = {}) { 11      options.init = options.init 12        ? [vuexInit].concat(options.init) 13        : vuexInit 14      _init.call(this, options) 15    } 16  } 17  /** 18   * Vuex init hook, injected into each instances init hooks list. 19   */ 20 21  function vuexInit () { 22    const options = this.$options 23    // store injection 24    if (options.store) { 25      this.$store = typeof options.store === 'function' 26        ? options.store() 27        : options.store 28    } else if (options.parent && options.parent.$store) { 29      this.$store = options.parent.$store 30    } 31  } 32} 33

我们看到 在 Vue 2.0 以上版本,通过 Vue.mixin({ beforeCreate: vuexInit })实现了在每一个实例的 beforeCreate 生命周期调用vuexInit 方法。

而 vuexInit 方法则是:在跟节点我们会直接把store 注入,在其他节点则拿父级节点的 store,这样this.$store 永远是你在根节点注入的那个store。

vue mixin 源码实现

在 Vuex 的例子中,我们通过 Vue.mixin({ beforeCreate: vuexInit }) 实现对实例的 $store 扩展。

全局 mixin 注册

我们先看一下 mixin 是如何挂载到原型上的。

src/core/index.js 中:

1import Vue from './instance/index' 2import { initGlobalAPI } from './global-api/index' 3 4initGlobalAPI(Vue) 5 6export default Vue 7

我们发现有一个 initGlobalAPI。在 src/global-api/index 中:

1/* @flow */ 2 3import config from '../config' 4import { initUse } from './use' 5import { initMixin } from './mixin' 6import { initExtend } from './extend' 7import { initAssetRegisters } from './assets' 8import { set, del } from '../observer/index' 9import { ASSET_TYPES } from 'shared/constants' 10import builtInComponents from '../components/index' 11 12import { 13  warn, 14  extend, 15  nextTick, 16  mergeOptions, 17  defineReactive 18} from '../util/index' 19 20export function initGlobalAPI (Vue: GlobalAPI) { 21  // config 22  const configDef = {} 23  configDef.get = () => config 24  if (process.env.NODE_ENV !== 'production') { 25    configDef.set = () => { 26      warn( 27        'Do not replace the Vue.config object, set individual fields instead.' 28      ) 29    } 30  } 31  Object.defineProperty(Vue, 'config', configDef) 32 33  // exposed util methods. 34  // NOTE: these are not considered part of the public API - avoid relying on 35  // them unless you are aware of the risk. 36  Vue.util = { 37    warn, 38    extend, 39    mergeOptions, 40    defineReactive 41  } 42 43  Vue.set = set 44  Vue.delete = del 45  Vue.nextTick = nextTick 46 47  Vue.options = Object.create(null) 48  ASSET_TYPES.forEach(type => { 49    Vue.options[type + 's'] = Object.create(null) 50  }) 51 52  // this is used to identify the "base" constructor to extend all plain-object 53  // components with in Weex's multi-instance scenarios. 54  Vue.options._base = Vue 55 56  extend(Vue.options.components, builtInComponents) 57 58  initUse(Vue) 59  initMixin(Vue) 60  initExtend(Vue) 61  initAssetRegisters(Vue) 62} 63

所有全局的方法都在这里注册。我们关注 initMixin 方法,定义在 src/core/global-api/mixin.js:

1import { mergeOptions } from '../util/index' 2 3export function initMixin (Vue: GlobalAPI) { 4  Vue.mixin = function (mixin: Object) { 5    this.options = mergeOptions(this.options, mixin) 6    return this 7  } 8} 9

至此我们发现了 Vue 如何挂载全局 mixin。

mixin 合并策略

vuex 通过 beforeCreate Hook 实现为所有 vm 添加 $store 实例。让我们先把 hook 的事情放一边。看一看 beforeCreate 如何实现。

src/core/instance/init.js 中:

1export function initMixin (Vue: Class<Component>) { 2  Vue.prototype._init = function (options?: Object) { 3    // remove unrelated code 4    initLifecycle(vm) 5    initEvents(vm) 6    initRender(vm) 7    callHook(vm, 'beforeCreate') 8    initInjections(vm) // resolve injections before data/props 9    initState(vm) 10    initProvide(vm) // resolve provide after data/props 11    callHook(vm, 'created') 12 13    // remove unrelated code 14    if (vm.$options.el) { 15      vm.$mount(vm.$options.el) 16    } 17  } 18} 19

我们可以看到在 initRender 完成后,会调用 callHook(vm, 'beforeCreate')。而 init 实在 vue 实例化会执行的。

src/core/instance/lifecycle.js 中:

1export function callHook (vm: Component, hook: string) { 2  // #7573 disable dep collection when invoking lifecycle hooks 3  pushTarget() 4  const handlers = vm.$options[hook] 5  if (handlers) { 6    for (let i = 0, j = handlers.length; i < j; i++) { 7      try { 8        handlers[i].call(vm) 9      } catch (e) { 10        handleError(e, vm, `${hook} hook`) 11      } 12    } 13  } 14  if (vm._hasHookEvent) { 15    vm.$emit('hook:' + hook) 16  } 17  popTarget() 18} 19 20

在对 beforeCreate 执行 callHook 过程中,会先从 vue 实例的 options 中取出所有挂载的 handlers。然后循环调用 call 方法执行所有的 hook:

1handlers[i].call(vm) 2

由此我们可以了解到全局的 hook mixin 会和要 mixin 的组件合并 hook,最后生成一个数组。

回头再看:

1import { mergeOptions } from '../util/index' 2 3export function initMixin (Vue: GlobalAPI) { 4  Vue.mixin = function (mixin: Object) { 5    this.options = mergeOptions(this.options, mixin) 6    return this 7  } 8} 9

this.options 默认是 vue 内置的一些 option:

mixin 就是你要混入的对象。我们来看一看 mergeOptions。定义在 src/core/util/options.js:

1export function mergeOptions ( parent: Object, 2  child: Object, 3  vm?: Component): Object { 4  if (process.env.NODE_ENV !== 'production') { 5    checkComponents(child) 6  } 7 8  if (typeof child === 'function') { 9    child = child.options 10  } 11 12  normalizeProps(child, vm) 13  normalizeInject(child, vm) 14  normalizeDirectives(child) 15  const extendsFrom = child.extends 16  if (extendsFrom) { 17    parent = mergeOptions(parent, extendsFrom, vm) 18  } 19  if (child.mixins) { 20    for (let i = 0, l = child.mixins.length; i < l; i++) { 21      parent = mergeOptions(parent, child.mixins[i], vm) 22    } 23  } 24  const options = {} 25  let key 26  for (key in parent) { 27    mergeField(key) 28  } 29  for (key in child) { 30    if (!hasOwn(parent, key)) { 31      mergeField(key) 32    } 33  } 34  function mergeField (key) { 35    const strat = strats[key] || defaultStrat 36    options[key] = strat(parent[key], child[key], vm, key) 37  } 38  return options 39} 40

忽略不相干代码我们直接跳到:

1 for (key in child) { 2    if (!hasOwn(parent, key)) { 3      mergeField(key) 4    } 5  } 6  function mergeField (key) { 7    const strat = strats[key] || defaultStrat 8    options[key] = strat(parent[key], child[key], vm, key) 9  }

此时 child 为 { beforeCreate: vuexInit }。走入到 mergeField 流程。mergeField 先取合并策略。

const strat = strats[key] || defaultStrat,相当于取 strats['beforeCreate'] 的合并策略。定义在通文件的上方:

1/** 2 * Hooks and props are merged as arrays. 3 */ 4function mergeHook ( parentVal: ?Array<Function>, 5  childVal: ?Function | ?Array<Function>): ?Array<Function> { 6  return childVal 7    ? parentVal 8      ? parentVal.concat(childVal) 9      : Array.isArray(childVal) 10        ? childVal 11        : [childVal] 12    : parentVal 13} 14 15LIFECYCLE_HOOKS.forEach(hook => { 16  strats[hook] = mergeHook 17}) 18 19// src/shared/constants.js 20 21export const LIFECYCLE_HOOKS = [ 22  'beforeCreate', 23  'created', 24  'beforeMount', 25  'mounted', 26  'beforeUpdate', 27  'updated', 28  'beforeDestroy', 29  'destroyed', 30  'activated', 31  'deactivated', 32  'errorCaptured' 33] 34

在  mergeHook 中的合并策略是把所有的 hook 生成一个函数数组。其他相关策略可以在options 文件中查找(如果是对象,组件本身的会覆盖上层,data 会执行结果,返回再merge,hook则生成数组)。

mixin 早于实例化

mergeOptions 会多次调用,正如其注释说描述的那样:

1/** 2 * Merge two option objects into a new one. 3 * Core utility used in both instantiation and inheritance. 4 */ 5

上面介绍了全局 mixin 的流程,我们来看下 实例化部分的流程。在 src/core/instance/init.js 中:

1export function initMixin (Vue: Class<Component>) { 2  Vue.prototype._init = function (options?: Object) { 3    if (options && options._isComponent) { 4      // optimize internal component instantiation 5      // since dynamic options merging is pretty slow, and none of the 6      // internal component options needs special treatment. 7      initInternalComponent(vm, options) 8    } else { 9      vm.$options = mergeOptions( 10        resolveConstructorOptions(vm.constructor), 11        options || {}, 12        vm 13      ) 14    } 15    // expose real self 16    vm._self = vm 17    initLifecycle(vm) 18    initEvents(vm) 19    initRender(vm) 20    callHook(vm, 'beforeCreate') 21    initInjections(vm) // resolve injections before data/props 22    initState(vm) 23    initProvide(vm) // resolve provide after data/props 24    callHook(vm, 'created') 25    if (vm.$options.el) { 26      vm.$mount(vm.$options.el) 27    } 28  } 29} 30

由于 全局 mixin 通常放在最上方。所以一个 vue 实例,通常是内置的 options + 全局 mixin 的 options +用户自定义options,加上合并策略生成最终的 options.

那么对于 hook 来说是[mixinHook, userHook]。mixin 的hook 函数优先于用户自定义的 hook 执行。

local mixin

在 组件中书写 mixin 过程中:

1const Tooltip = { 2  template: '#tooltip', 3  mixins: [toggleMixin], 4}; 5

在 mergeOptions 的过程中有下面一段代码:

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

当 tooltip 实例化时,会将对应的参数 merge 到实例中。

定制合并策略

1Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) { 2  // return mergedVal 3}

来自:flyyang's Blog

链接:https://flyyang.me/2019/01/24/vue-mixin/

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )