你也許不知道的Vuejs - 最佳實踐(2)

yugasun發表於2019-02-28

by yugasun from yugasun.com/post/you-ma… 本文可全文轉載,但需要保留原作者和出處。

我們在實際開發過程中,當專案越來越大,元件越來越豐富時,經常會面臨一個問題:很多元件會公用一些通用的 propsdatamethods等宣告,但是也會摻雜元件自己的一些私有特有宣告,那麼我們能不能像類的繼承一樣,來提煉和繼承呢? 當然這是可以的,這裡可以通過兩個基本 API extendsmixins 來實現。這兩個API是可以相互替換的,唯一的區別是,extends 屬性接受的通常是個單一元件物件,而 mixins 屬性接受的是個元件物件陣列。當他們只繼承單一元件時,是可以互換的。由於本人開發中,習慣使用 mixins,所以本文所有例項均使用 mixins 來實現。

mixins

先來看看官方介紹:

mixins 選項接受一個混入物件的陣列。這些混入例項物件可以像正常的例項物件一樣包含選項,他們將在 Vue.extend() 裡最終選擇使用相同的選項合併邏輯合併。舉例:如果你的混入包含一個鉤子而建立元件本身也有一個,兩個函式將被呼叫。

簡單的理解就是Vue例項中的所有屬性配置可以通過 mixins 實現繼承。

簡單示例如下:

var mixin = {
  created: function () { console.log(1) }
}
var vm = new Vue({
  created: function () { console.log(2) },
  mixins: [mixin]
})
// => 1
// => 2
複製程式碼

案例1

假設有這麼個需求: 在某個元件渲染後向伺服器端傳送一個請求,進行打點,好的,很快我們想到 mounted 鉤子函式,然後快速的實現了需求,程式碼如下:

export default {
  name: 'comp1',
  // ...
  mounted() {
    console.log('Component comp1 mounted');
  }
  // ...
}
複製程式碼

然後某一天需求變成了 某幾個 元件需要進行打點,好的,我們又進行了一頓猛如虎的操作,將上面程式碼複製到每個打點的元件,很快把需求搞定了。可是噩夢才剛剛開始,過了幾天需求又變了,除了在元件渲染後需要打點,同時還需要在 created 後打點..... 此種場景是不是像極了愛情,面對現實我們總是在不停屈服,最終還是忍痛把需求做了。

回頭冷靜思考下,其實這個打點是很普遍的需求。如果從頭來過,我們一定會選擇用繼承的方式來實現,而不是盲目的去愛,哦不,盲目的複製貼上。因為我們有 mixins,只需要編寫一次,到處可用。那就讓我們從頭再來一次,首先建立一個 src/minins/log.js 檔案:

export default {
  created() {
    console.log(`Component ${this.$options.name} created.`);
  },
  mounted() {
    console.log(`Component ${this.$options.name} mounted.`);
  },
};
複製程式碼

然後在你需要的任何一個元件中引入使用:

import logMixin from '@/mixins/log';

export default {
  name: 'comp1',
  mixins: [logMixin],
  // ...
}
複製程式碼

一番修改後,你會發現產品經理妹子也可以那麼迷人,是不是你又開始相信愛情了......

執行專案,開啟控制檯輸出如下:

Component comp1 created.
Component comp2 created.
複製程式碼

案例2

上面的需求是元件打點,現在我們新增了需求,需要給某幾個元件新增一個通用方法 sayHellomethods 中,並在元件渲染後呼叫,但是隻是上面打點的部分元件需要新增此功能,雖然只是部分元件,但也有個上百個吧(誇張手法,切勿模仿)。聽到這裡,你默默推開了身邊的產品妹子,拒絕道:對不起,我已經不相信愛情了。此時,有個聲音在輕聲的嘀咕著:你還可以相信的!

好的,那麼,我就再讓你相信一次。首先新增檔案 src/mixins/func.js:

export default {
  mounted() {
    this.sayHello();
  },
  methods: {
    sayHello() {
      console.log('產品妹子,你好美!');
    },
  },
};
複製程式碼

然後在需要的元件中引入就行了:

import logMixin from '@/mixins/log';
import funcMixin from '@/mixins/func';

export default {
  name: 'comp1',
  mixins: [logMixin, funcMixin],
  // ...
}
複製程式碼

執行專案,開啟控制檯輸出如下:

Component comp1 created.
Component comp2 created.
Component comp1 mounted.
產品妹子,你好美!
Component comp2 mounted.
複製程式碼

案例3

好了,你終於可以跟產品妹子一起在夕陽下愉快地奔跑了。突然有一天,元件渲染後打點,成了公司的規範,也就是你編寫的所有元件都需要打點了,產品妹子很無奈的看著你說:這不是我想要的結果,是你做的太優秀,被公司提上了日程,寫入了編碼規範.....可現實就是這樣,你總想逃,卻逃不掉......

其實你還可以逃的,Vue.mixin 說。

全域性註冊一個混入,影響註冊之後所有建立的每個 Vue 例項。外掛作者可以使用混入,向元件注入自定義的行為。不推薦在應用程式碼中使用

這不就是你一直追尋的愛情嗎?於是你移除了之前引入的 logMixin,然後默默地在入口檔案(src/main.js)中寫下了愛情的宣言:

//...
Vue.mixin({
  created() {
    console.log(`Component ${this.$options.name} created from 全域性打點`);
  },
  mounted() {
    console.log(`Component ${this.$options.name} mounted from 全域性打點`);
  },
});

// new Vue....
複製程式碼

執行專案,開啟控制檯輸出如下:

Component undefined created from 全域性打點
Component App created from 全域性打點
Component Index created from 全域性打點
Component router-link created from 全域性打點
Component comp1 created from 全域性打點
Component comp1 created.
Component comp2 created from 全域性打點
Component comp2 created.
Component comp3 created from 全域性打點
Component router-link mounted from 全域性打點
Component comp1 mounted from 全域性打點
Component comp1 mounted.
產品妹子,你好美!
Component comp2 mounted from 全域性打點
Component comp2 mounted.
Component comp3 mounted from 全域性打點
Component Index mounted from 全域性打點
Component App mounted from 全域性打點
Component undefined mounted from 全域性打點
複製程式碼

你會發現所有的 Vue 元件都注入了打點。

原理解析

其實 mixins 用起來非常簡單,但是其背後的原理,還是值得我們去深究的:

  1. 為什麼 mixins 後,鉤子函式是依次執行的,而不是替換?
  2. 為什麼 mixins 後,自身 data 屬性優於混入屬性?

要想回答上面的問題,我們得從 vue 原始碼開始說起。

Vue 在初始化 mixin 的時候,對於不同的屬性,採用的策略是不同的,初始化程式碼在檔案 src/core/global-api.js 中, 如下:

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

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    Vue.options = mergeOptions(Vue.options, mixin)
  }
}
複製程式碼

你會發現是通過 mergeOptions 函式來進行合併的,它在檔案 src/core/util/options.js, 它的原始碼如下:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // 省略不必要程式碼
  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
}
複製程式碼

這個函式很好理解,大概做的事情就是將 child 的屬性合入到 parent 中,不同屬性採用了不同的策略,這些策略都定義在 strats 物件上。

我們先看看 生命週期函式 的合併策略,程式碼如下:

/**
 * Hooks and param attributes 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
}
複製程式碼

可以發現 Vue 例項的生命週期函式最終都賦值成了一個陣列,並對 mixins 中的進行了陣列合並。這就是為什麼元件 mixins 後的生命週期函式是依次執行的原因。

同樣再來看看 data 的合入策略:

/**
 * Helper that recursively merges two data objects together.
 */
function mergeData (to: Object, from: ?Object): Object {
  let key, toVal, fromVal
  for (key in from) {
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (isObject(toVal) && isObject(fromVal)) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}
複製程式碼

這個過程就是物件屬性的合併,但是 to 上的優先順序是高於 from 的,這就是為什麼我們在對一個元件進行 mixins 的時候,自身 data 優先順序高於混入的 data 屬性,也就是如果 mixins 中和自身均含有相同屬性時,混入屬性值不會被新增到當前元件中。

感興趣的同學,還可以去研究下其他屬性的混入策略,原始碼均在 src/core/util/options.js 中,也很好理解。

總結

越是簡單的東西,越是把雙刃劍,實際使用中一定要注意,特別是全域性性的混入,這會帶來效能開銷。大家可以多編寫,多總結,找到最合適的使用習慣就好,建議多閱讀著名開源專案的原始碼,你會從中學到更多前輩們的技巧。

原始碼在此

專題目錄

You-May-Not-Know-Vuejs

相關文章