Vue Mixin

2021-03-02 JavaScript開發寶典

mixin, 意為混入。

代碼表示

假設把你已有的奶油味的稱為 base,把要添加的味道稱為 mixins。用 js 偽代碼可以這麼來寫:

const base = {
  hasCreamFlavor() {
    return true;
  }
}
const mixins = {
  hasVanillaFlavor() {
    return true;
  },
  hasStrawberryFlavor() {
    return true;
 }
}

function mergeStrategies(base, mixins) {
  return Object.assign({}, base, mixins);
}
// newBase 就擁有了三種口味。
const newBase = mergeStrategies(base, mixins);

注意一下這個 mergeStrategies。

合併策略可以你想要的形式,也就是說你可以自定義自己的策略,這是其一。另外要解決衝突的問題。上面是通過 Object.assign 來實現的,那麼 mixins 內的方法會覆蓋base 內的內容。如果這不是你期望的結果,可以調換 mixin 和 base 的位置。

組合大於繼承 && DRY

想像一下上面的例子用繼承如何實現?由於 js 是單繼承語言,只能一層層繼承。寫起來很繁瑣。這裡就體現了 mixin 的好處。符合組合大於繼承的原則。

mixin 內通常是提取了公用功能的代碼。而不是每一個地方都寫一遍。符合 DRY 原則。

什麼是 vue mixin

vue mixin 是針對組件間功能共享來做的。可以對組件的任意部分(生命周期, data等)進行mixin,但不同的 mixin 之後的合併策略不同。在源碼分析部分會介紹細節。

組件級 mixin

假設兩個功能組件 model 和 tooltip ,他們都有一個顯示和關閉的 toggle 動作:

//modal
const Modal = {
  template: '#modal',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

//tooltip
const Tooltip = {
  template: '#tooltip',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

可以用 mixin 這麼寫:

const toggleMixin = {
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

const Modal = {
  template: '#modal',
  mixins: [toggleMixin]
};

const Tooltip = {
  template: '#tooltip',
  mixins: [toggleMixin],
};

全局 mixin

全局 mixin 會作用到每一個 vue 實例上。所以使用的時候要慎重。通常會用 plugin 來顯示的聲明用到了那些 mixin。

比如 vuex。我們都知道它在每一個實例上擴展了一個 

在 src/mixin.js 內

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }
  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

我們看到 在 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 中:

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'

initGlobalAPI(Vue)

export default Vue

我們發現有一個 initGlobalAPI。在 src/global-api/index 中:

/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

所有全局的方法都在這裡註冊。我們關注 initMixin 方法,定義在 src/core/global-api/mixin.js:

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

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

至此我們發現了 Vue 如何掛載全局 mixin。

mixin 合併策略

vuex 通過 beforeCreate Hook 實現為所有 vm 添加 $store 實例。讓我們先把 hook 的事情放一邊。看一看 beforeCreate 如何實現。

在 src/core/instance/init.js 中:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // remove unrelated code
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    // remove unrelated code
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

我們可以看到在 initRender 完成後,會調用 callHook(vm, 'beforeCreate')。而 init 是在 vue 實例化會執行的。

在 src/core/instance/lifecycle.js 中:

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

在對 beforeCreate 執行 callHook 過程中,會先從 vue 實例的 options 中取出所有掛載的 handlers。然後循環調用 call 方法執行所有的 hook:

handlers[i].call(vm)

由此我們可以了解到全局的 hook mixin 會和要 mixin 的組件合併 hook,最後生成一個數組。

回頭再看:

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

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

this.options 默認是 vue 內置的一些 option:

image

mixin 就是你要混入的對象。我們來看一看 mergeOptions。定義在 src/core/util/options.js:

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)
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, 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
}

忽略不相干代碼我們直接跳到:

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

此時 child 為 { beforeCreate: vuexInit }。走入到 mergeField 流程。mergeField 先取合併策略。

const strat = strats[key] || defaultStrat,相當於取 strats['beforeCreate'] 的合併策略。定義在文件的上方:

/**
 * Hooks and props are merged as arrays.
 */
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

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

// src/shared/constants.js

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]

在  mergeHook 中的合併策略是把所有的 hook 生成一個函數數組。其他相關策略可以在options 文件中查找(如果是對象,組件本身的會覆蓋上層,data 會執行結果,返回再merge,hook則生成數組)。

mixin 早於實例化

mergeOptions 會多次調用,正如其注釋描述的那樣:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */

上面介紹了全局 mixin 的流程,我們來看下 實例化部分的流程。在 src/core/instance/init.js 中:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

由於 全局 mixin 通常放在最上方。所以一個 vue 實例,通常是內置的 options + 全局 mixin 的 options +用戶自定義options,加上合併策略生成最終的 options.

那麼對於 hook 來說是[mixinHook, userHook]。mixin 的hook 函數優先於用戶自定義的 hook 執行。

local mixin

在 組件中書寫 mixin 過程中:

const Tooltip = {
  template: '#tooltip',
  mixins: [toggleMixin],
};

在 mergeOptions 的過程中有下面一段代碼:

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

當 tooltip 實例化時,會將對應的參數 merge 到實例中。

定製合併策略
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
  // return mergedVal
}

以上。

參考http://techsith.com/mixins-in-javascript/https://vuejs.org/v2/guide/mixins.htmlhttps://css-tricks.com/using-mixins-vue-js/

相關焦點

  • 深入淺出 Vue Mixin
    mixin 內通常是提取了公用功能的代碼。而不是每一個地方都寫一遍。符合 DRY 原則。什麼是 vue mixinvue mixin 是針對組件間功能共享來做的。可以對組件的任意部分(生命周期, data等)進行mixin,但不同的 mixin 之後的合併策略不同。
  • 【Vuejs】887- 深入淺出 Vue Mixin
    mixin 內通常是提取了公用功能的代碼。而不是每一個地方都寫一遍。符合 DRY 原則。什麼是 vue mixinvue mixin 是針對組件間功能共享來做的。可以對組件的任意部分(生命周期, data等)進行mixin,但不同的 mixin 之後的合併策略不同。
  • 擴展微信小程序 Mixins 實現
    >將 mixins 中每一個混入對象都合併到 options 選項對象中,在開始 mergeOptions 函數之前,先來規定一下 mixins 的合併規則。isPlainObject(mixin)) {      throw new Error("typeof mixin must be plain object")     }    // 混入對象中嵌套混入對象,遞歸合併    if (mixin.mixins) {      mixin = mergeOptions(mixin.mixins, mixin
  • vue2與vue3的差異(總結)
    意味著以後可以通過 vue, Dom 編程的方式來進行canvas、webgl 編程10.全局 Vue API 已更改為使用應用程式實例vue2使用全局api 如 Vue.component, Vue.mixin, Vue.use等,缺點是會導致所創建的根實例將共享相同的全局配置(從相同的 Vue 構造函數創建的每個根實例都共享同一套全局環境。這樣就導致一個問題,只要某一個根實例對 全局 API 和 全局配置做了變動,就會影響由相同 Vue 構造函數創建的其他根實例。)
  • 從0 到 1 實現一個框架BootstrapVue: Lesson 2
    這部分和 vue-router 裡面定義的部分基本一致.})先看到 attrsMixin, 這個 mixin 的作用主要是把$attrs代理到bvAttrsimport { makePropCacheMixin } from "..
  • Vue全家桶之Vue2.X和Vue3.X腳手架創建項目的不同方式
    今天來介紹一些如何使用vue腳手架來創建項目通過腳手架Vue cli 腳手架來創建項目有三種方式:1.基於 交互式命令行 的方式,創建 Vue項目 使用命令 vue create my-project (基於Vue cli 3.X以上版本 npm install
  • Vue.js布局
    動態Vue.js布局組件前言vue.js是漸進增強的視圖庫,可以作為.html頁面部分使用,也可以結合vue-router、vuex、axios用來構建單頁面或多頁面應用。
  • vue.js第七課
    vue組件的學習(四)今天我們來了解下vue的插槽。插槽就是我們在子組件裡面用slot標籤提供了個佔位符,父組件可以在這個佔位符中顯示內容。
  • vue-cli安裝步驟詳解
    當然啦,關於wenpack的只是也是一個大體系,為什麼我們需要和webpack一起用呢,這是因為,我們用的單文件組件的時候,需要用vue-loader來解析.vue後綴的文件,還有其他不同類型文件也需要不同的loader來解析,所以vue的項目最好配合webpack。
  • 【Vuejs】802- 如何區別 Vue2 和 Vue3 ?
    $emit(vue3中只能在methods裡使用)作用相同import {SetupContext } from 'vue'setup(props: Prop, context: SetupContext) {    const toggle = () => {      context.emit('input
  • Vue 3 的最新進展
    Vue Router目前存在部分與 vue-router@3.x 相關的路由鉤子(router hook)行為一致性問題,這也是 Vue Router 沒有被標記為 Beta 的原因。不過在非關鍵項目上可以使用新的路由。
  • Vue 3.0 Beta
    進入 Beta 階段即意味著: 已合併所有計劃內的 RFC 已實現所有被合併的 RFC Vue CLI 現在通過 vue-cli-plugin-vue-next獲取 Vue 3.0 Beta https://github.com/vuejs/vue-next/releases
  • Vue 3.0 語法快速入門
    vue-next#status-beta雖然目前是beta版本,但我們依然可以嘗鮮,在本地創建Vue項目,並做一做Demo;一、創建項目// 先升級vue-cli到4.x版本  cnpm install -g @vue/cli
  • 如何使用vue3搭建項目框架並運行
    打開Git命令窗口2、使用npm命令安裝vue/cli,這裡需要使用--force;由於之前安裝了vue2下的vue cli,需要覆蓋之前的npm install -g @vue/cli --force
  • vue-devtools調試工具安裝與使用的簡單教程
    使用vue開發項目時,如果習慣vue當前項目一些操作後相關數據的變化的log,可以安裝一個vue-devtools調試工具,如何安裝呢?請看下邊:一:下載與安裝:1.下載好vue-devtools壓縮包(crx類型的壓縮包),直接解壓到你自己選擇的本地目錄中:2.打開谷歌瀏覽器,打開設置,並找到擴展程序:3.
  • vue中v-model詳解
    (一)vue中經常使用到<input>和<textarea>這類表單元素。
  • 一個超級棒的VUE流程設計器
    今天小編推薦一款流程設計器easy-flow, easy-flow基於VUE+ElementUI+JsPlumb的流程設計器,通過 vuedraggable 插件來實現節點拖拽。功能介紹 如何集成 在自己的vue工程中找到package.json,並引入如下依賴(不用額外引入jsplumb)  "element-ui": "2.9.1",  "lodash": "4.17.15",  "vue": "^2.5.2",  "vue-codemirror": "^4.0.6",  "vuedraggable": "
  • 如何在vue項目中使用sass並設置元素樣式
    GitnpmHBuilder瀏覽器技術vue那麼,如何在vue項目中使用sass?下面利用實例說明:1、打開HBuilderX開發工具,新建項目,選擇普通項目;然後輸入項目名稱wmk,選擇vue模板,接著點擊創建按鈕
  • Vue.js 很難學?看看這個由 DCloud 與 Vue 官方合作推出的免費入門...
    視頻教程目錄序言 vue.js介紹第1節 安裝與部署第2節 創建第一個vue應用第3節 數據與方法第4節 生命周期第5節 模板語法-插值第6節 模板語法-指令第7節 class與style綁定第8節 條件渲染第9節 列表渲染第10節 事件綁定第11節 表單輸入綁定第12節 組件基礎第13節 組件註冊第14節 單文件組件第15節 免終端開發vue應用視頻截圖
  • 用Vue 和Bootstrap 4 來構建Web前端界面
    首先我們安裝Vue Cli安裝Vue Cli由於npm安裝較慢,甚至會失敗,需要,先安裝國內鏡像,可以使用cnpm或者npm別稱:然後用cnpm安裝vue.jscnpm install -g @vue.js