Vue中使用裝飾器,我是認真的

產品上線事繁多,測試產品催不離。
休問Bug剩多少,眼圈如漆身如泥。

做爲一個曾經的Java coder, 當我第一次看到js裏面的裝飾器(Decorator)的時候,就立刻想到了Java中的註解,固然在實際原理和功能上面,Java的註解和js的裝飾器仍是有很大差異的。本文題目是Vue中使用裝飾器,我是認真的,但本文將從裝飾器的概念開發聊起,一塊兒來看看吧。javascript

經過本文內容,你將學到如下內容:前端

  1. 瞭解什麼是裝飾器
  2. 在方法使用裝飾器
  3. class中使用裝飾器
  4. Vue中使用裝飾器
本文首發於公衆號【前端有的玩】,不想當鹹魚,想要換工做,關注公衆號,帶你每日一塊兒刷大廠面試題,關注 === 大廠 offer

什麼是裝飾器

裝飾器是ES2016提出來的一個提案,當前處於Stage 2階段,關於裝飾器的體驗,能夠點擊 https://github.com/tc39/proposal-decorators查看詳情。裝飾器是一種與類相關的語法糖,用來包裝或者修改類或者類的方法的行爲,其實裝飾器就是設計模式中裝飾者模式的一種實現方式。不過前面說的這些概念太乾了,咱們用人話來翻譯一下,舉一個例子。vue

在平常開發寫bug過程當中,咱們常常會用到防抖和節流,好比像下面這樣java

class MyClass {
  follow = debounce(function() {
    console.log('我是子君,關注我哦')
  }, 100)
}

const myClass = new MyClass()
// 屢次調用只會輸出一次
myClass.follow()
myClass.follow()

上面是一個防抖的例子,咱們經過debounce函數將另外一個函數包起來,實現了防抖的功能,這時候再有另外一個需求,好比但願在調用follow函數先後各打印一段日誌,這時候咱們還能夠再開發一個log函數,而後繼續將follow包裝起來git

/**
 * 最外層是防抖,不然log會被調用屢次
 */
class MyClass {
  follow = debounce(
    log(function() {
      console.log('我是子君,關注我哦')
    }),
    100
  )
}

上面代碼中的debouncelog兩個函數,本質上是兩個包裝函數,經過這兩個函數對原函數的包裝,使原函數的行爲發生了變化,而js中的裝飾器的原理就是這樣的,咱們使用裝飾器對上面的代碼進行改造github

class MyClass {
  @debounce(100)
  @log
  follow() {
    console.log('我是子君,關注我哦')
  }
}

裝飾器的形式就是 @ + 函數名,若是有參數的話,後面的括號裏面能夠傳參面試

在方法上使用裝飾器

裝飾器能夠應用到class上或者class裏面的屬性上面,但通常狀況下,應用到class屬性上面的場景會比較多一些,好比像上面咱們說的log,debounce等等,都通常會應用到類屬性上面,接下來咱們一塊兒來具體看一下如何實現一個裝飾器,並應用到類上面。在實現裝飾器以前,咱們須要先了解一下屬性描述符vue-cli

瞭解一下屬性描述符

在咱們定義一個對象裏面的屬性的時候,其實這個屬性上面是有許多屬性描述符的,這些描述符標明瞭這個屬性能不能修改,能不能枚舉,能不能刪除等等,同時ECMAScript將這些屬性描述符分爲兩類,分別是數據屬性和訪問器屬性,而且數據屬性與訪問器屬性是不能共存的。設計模式

數據屬性

數據屬性包含一個數據值的位置,在這個位置能夠讀取和寫入值。數據屬性包含了四個描述符,分別是app

  1. configurable

    表示能不能經過delete刪除屬性,可否修改屬性的其餘描述符特性,或者可否將數據屬性修改成訪問器屬性。當咱們經過let obj = {name: ''}聲明一個對象的時候,這個對象裏面全部的屬性的configurable描述符的值都是true

  2. enumerable

    表示能不能經過for in或者Object.keys等方式獲取到屬性,咱們通常聲明的對象裏面這個描述符的值是true,可是對於class類裏面的屬性來講,這個值是false

  3. writable

    表示可否修改屬性的數據值,經過將這個修改成false,能夠實現屬性只讀的效果。

  4. value

    表示當前屬性的數據值,讀取屬性值的時候,從這裏讀取;寫入屬性值的時候,會寫到這個位置。

訪問器屬性

訪問器屬性不包含數據值,他們包含了gettersetter兩個函數,同時configurableenumerable是數據屬性與訪問器屬性共有的兩個描述符。

  1. getter

    在讀取屬性的時候調用這個函數,默認這個函數爲undefined

  2. setter

    在寫入屬性值的時候調用這個函數,默認這個函數爲undefined

瞭解了這六個描述符以後,你可能會有幾個疑問: 我如何去定義修改這些屬性描述符?這些屬性描述符與今天的文章主題有什麼關係?接下來是揭曉答案的時候了。

使用Object.defineProperty

瞭解過vue2.0雙向綁定原理的同窗必定知道,Vue的雙向綁定就是經過使用Object.defineProperty去定義數據屬性的gettersetter方法來實現的,好比下面有一個對象

let obj = {
  name: '子君',
  officialAccounts: '前端有的玩'
}

我但願這個對象裏面的用戶名是不能被修改的,用Object.defineProperty該如何定義呢?

Object.defineProperty(obj,'name', {
  // 設置writable 是 false, 這個屬性將不能被修改
  writable: false
})
// 修改obj.name
obj.name = "君子"
// 打印依然是子君
console.log(obj.name)

經過Object.defineProperty能夠去定義或者修改對象屬性的屬性描述符,可是由於數據屬性與訪問器屬性是互斥的,因此一次只能修改其中的一類,這一點須要注意。

定義一個防抖裝飾器

裝飾器本質上依然是一個函數,不過這個函數的參數是固定的,以下是防抖裝飾器的代碼

/**
*@param wait 延遲時長
*/
function debounce(wait) {
  return function(target, name, descriptor) {
    descriptor.value = debounce(descriptor.value, wait)
  }
}
// 使用方式
class MyClass {
  @debounce(100)
  follow() {
    console.log('我是子君,個人公衆號是 【前端有的玩】,關注有驚喜哦')
  }
}

咱們逐行去分析一下代碼

  1. 首先咱們定義了一個 debounce函數,同時有一個參數wait,這個函數對應的就是在下面調用裝飾器時使用的@debounce(100)
  2. debounce函數返回了一個新的函數,這個函數即裝飾器的核心,這個函數有三個參數,下面逐一分析

    1. target: 這個類屬性函數是在誰上面掛載的,如上例對應的是MyClass
    2. name: 這個類屬性函數的名稱,對應上面的follow
    3. descriptor: 這個就是咱們前面說的屬性描述符,經過直接descriptor上面的屬性,便可實現屬性只讀,數據重寫等功能
  3. 而後第三行 descriptor.value = debounce(descriptor.value, wait), 前面咱們已經瞭解到,屬性描述符上面的value對應的是這個屬性的值,因此咱們經過重寫這個屬性,將其用debounce函數包裝起來,這樣在函數調用follow時實際調用的是包裝後的函數

經過上面的三步,咱們就實現了類屬性上面可以使用的裝飾器,同時將其應用到了類屬性上面

class上使用裝飾器

裝飾器不只能夠應用到類屬性上面,還能夠直接應用到類上面,好比我但願能夠實現一個相似Vue混入那樣的功能,給一個類混入一些方法屬性,應該如何去作呢?

// 這個是要混入的對象
const methods = {
  logger() {
    console.log('記錄日誌')
  }
}

// 這個是一個登錄登出類
class Login{
  login() {}
  logout() {}
}

如何將上面的methods混入到Login中,首先咱們先實現一個類裝飾器

function mixins(obj) {
  return function (target) {
    Object.assign(target.prototype, obj)  
  }
}

// 而後經過裝飾器混入
@mixins(methods)
class Login{
  login() {}
  logout() {}
}

這樣就實現了類裝飾器。對於類裝飾器,只有一個參數,即target,對應的就是這個類自己。

瞭解完裝飾器,咱們接下來看一下如何在Vue中使用裝飾器。

Vue中使用裝飾器

使用ts開發Vue的同窗必定對vue-property-decorator不會感到陌生,這個插件提供了許多裝飾器,方便你們開發的時候使用,固然本文的中點不是這個插件。其實若是咱們的項目沒有使用ts,也是可使用裝飾器的,怎麼用呢?

配置基礎環境

除了一些老的項目,咱們如今通常新建Vue項目的時候,都會選擇使用腳手架vue-cli3/4來新建,這時候新建的項目已經默認支持了裝飾器,不須要再配置太多額外的東西,若是你的項目使用了eslint,那麼須要給eslint配置如下內容。

parserOptions: {
    ecmaFeatures:{
      // 支持裝飾器
      legacyDecorators: true
    }
  }

使用裝飾器

雖然Vue的組件,咱們通常書寫的時候export出去的是一個對象,可是這個並不影響咱們直接在組件中使用裝飾器,好比就拿上例中的log舉例。

function log() {
  /**
   * @param target 對應 methods 這個對象
   * @param name 對應屬性方法的名稱
   * @param descriptor 對應屬性方法的修飾符
   */
  return function(target, name, descriptor) {
    console.log(target, name, descriptor)
    const fn = descriptor.value
    descriptor.value = function(...rest) {
      console.log(`這是調用方法【${name}】前打印的日誌`)
      fn.call(this, ...rest)
      console.log(`這是調用方法【${name}】後打印的日誌`)
    }
  }
}

export default {
  created() {
    this.getData()
  },
  methods: {
    @log()
    getData() {
      console.log('獲取數據')
    }
  }
}

看了上面的代碼,是否是發如今Vue中使用裝飾器仍是很簡單的,和在class的屬性上面使用的方式如出一轍,但有一點須要注意,在methods裏面的方法上面使用裝飾器,這時候裝飾器的target對應的是methods

除了在methods上面可使用裝飾器以外,你也能夠在生命週期鉤子函數上面使用裝飾器,這時候target對應的是整個組件對象。

一些經常使用的裝飾器

下面小編羅列了幾個小編在項目中經常使用的幾個裝飾器,方便你們使用

1. 函數節流與防抖

函數節流與防抖應用場景是比較廣的,通常使用時候會經過throttledebounce方法對要調用的函數進行包裝,如今就可使用上文說的內容將這兩個函數封裝成裝飾器, 防抖節流使用的是lodash提供的方法,你們也能夠自行實現節流防抖函數哦

import { throttle, debounce } from 'lodash'
/**
 * 函數節流裝飾器
 * @param {number} wait 節流的毫秒
 * @param {Object} options 節流選項對象
 * [options.leading=true] (boolean): 指定調用在節流開始前。
 * [options.trailing=true] (boolean): 指定調用在節流結束後。
 */
export const throttle =  function(wait, options = {}) {
  return function(target, name, descriptor) {
    descriptor.value = throttle(descriptor.value, wait, options)
  }
}

/**
 * 函數防抖裝飾器
 * @param {number} wait 須要延遲的毫秒數。
 * @param {Object} options 選項對象
 * [options.leading=false] (boolean): 指定在延遲開始前調用。
 * [options.maxWait] (number): 設置 func 容許被延遲的最大值。
 * [options.trailing=true] (boolean): 指定在延遲結束後調用。
 */
export const debounce = function(wait, options = {}) {
  return function(target, name, descriptor) {
    descriptor.value = debounce(descriptor.value, wait, options)
  }
}

封裝完以後,在組件中使用

import {debounce} from '@/decorator'

export default {
  methods:{
    @debounce(100)
    resize(){}
  }
}

2. loading

在加載數據的時候,爲了個用戶一個友好的提示,同時防止用戶繼續操做,通常會在請求前顯示一個loading,而後在請求結束以後關掉loading,通常寫法以下

export default {
  methods:{
    async getData() {
      const loading = Toast.loading()
      try{
        const data = await loadData()
        // 其餘操做
      }catch(error){
        // 異常處理
        Toast.fail('加載失敗');
      }finally{
        loading.clear()
      }  
    }
  }
}

咱們能夠把上面的loading的邏輯使用裝飾器從新封裝,以下代碼

import { Toast } from 'vant'

/**
 * loading 裝飾器
 * @param {*} message 提示信息
 * @param {function} errorFn 異常處理邏輯
 */
export const loading =  function(message = '加載中...', errorFn = function() {}) {
  return function(target, name, descriptor) {
    const fn = descriptor.value
    descriptor.value = async function(...rest) {
      const loading = Toast.loading({
        message: message,
        forbidClick: true
      })
      try {
        return await fn.call(this, ...rest)
      } catch (error) {
        // 在調用失敗,且用戶自定義失敗的回調函數時,則執行
        errorFn && errorFn.call(this, error, ...rest)
        console.error(error)
      } finally {
        loading.clear()
      }
    }
  }
}

而後改造上面的組件代碼

export default {
  methods:{
    @loading('加載中')
    async getData() {
      try{
        const data = await loadData()
        // 其餘操做
      }catch(error){
        // 異常處理
        Toast.fail('加載失敗');
      }  
    }
  }
}

3. 確認框

當你點擊刪除按鈕的時候,通常都須要彈出一個提示框讓用戶確認是否刪除,這時候常規寫法多是這樣的

import { Dialog } from 'vant'

export default {
  methods: {
    deleteData() {
      Dialog.confirm({
        title: '提示',
        message: '肯定要刪除數據,此操做不可回退。'
      }).then(() => {
        console.log('在這裏作刪除操做')
      })
    }
  }
}

咱們能夠把上面確認的過程提出來作成裝飾器,以下代碼

import { Dialog } from 'vant'

/**
 * 確認提示框裝飾器
 * @param {*} message 提示信息
 * @param {*} title 標題
 * @param {*} cancelFn 取消回調函數
 */
export function confirm(
  message = '肯定要刪除數據,此操做不可回退。',
  title = '提示',
  cancelFn = function() {}
) {
  return function(target, name, descriptor) {
    const originFn = descriptor.value
    descriptor.value = async function(...rest) {
      try {
        await Dialog.confirm({
          message,
          title: title
        })
        originFn.apply(this, rest)
      } catch (error) {
        cancelFn && cancelFn(error)
      }
    }
  }
}

而後再使用確認框的時候,就能夠這樣使用了

export default {
  methods: {
    // 能夠不傳參,使用默認參數
    @confirm()
    deleteData() {
      console.log('在這裏作刪除操做')
    }
  }
}

是否是瞬間簡單多了,固然還能夠繼續封裝不少不少的裝飾器,由於文章內容有限,暫時提供這三個。

裝飾器組合使用

在上面咱們將類屬性上面使用裝飾器的時候,說道裝飾器能夠組合使用,在Vue組件上面使用也是同樣的,好比咱們但願在確認刪除以後,調用接口時候出現loading,就能夠這樣寫(必定要注意順序)

export default {
  methods: {
    @confirm()
    @loading()
    async deleteData() {
      await delete()
    }
  }
}
本節定義的裝飾器,均已應用到這個項目中 https://github.com/snowzijun/vue-vant-base, 這是一個基於 Vant開發的開箱即用移動端框架,你只須要 fork下來,無需作任何配置就能夠直接進行業務開發,歡迎使用,喜歡麻煩給一個 star

我是子君,今天就寫這麼多,本文首發於【前端有的玩】,這是一個專一於前端技術,前端面試相關的公衆號,同時關注以後即刻拉你加入前端交流羣,咱們一塊兒聊前端,歡迎關注。

結語

不要吹滅你的靈感和你的想象力; 不要成爲你的模型的奴隸。 ——文森特・梵高
相關文章
相關標籤/搜索