產品上線事繁多,測試產品催不離。
休問Bug剩多少,眼圈如漆身如泥。
作為一個曾經的Java coder
, 當我第一次看到js
裡面的裝飾器(Decorator
)的時候,就馬上想到了Java
中的註解,當然在實際原理和功能上面,Java
的註解和js
的裝飾器還是有很大差別的。本文題目是Vue中使用裝飾器,我是認真的
,但本文將從裝飾器的概念開發聊起,一起來看看吧。
通過本文內容,你將學到以下內容:
- 瞭解什麼是裝飾器
- 在方法使用裝飾器
- 在
class
中使用裝飾器 - 在
Vue
中使用裝飾器
本文首發於公眾號【前端有的玩】,不想當鹹魚,想要換工作,關注公眾號,帶你每日一起刷大廠面試題,關注===
大廠offer
。
什麼是裝飾器
裝飾器是ES2016
提出來的一個提案,當前處於Stage 2
階段,關於裝飾器的體驗,可以點選 https://github.com/tc39/proposal-decorators檢視詳情。裝飾器是一種與類相關的語法糖,用來包裝或者修改類或者類的方法的行為,其實裝飾器就是設計模式中裝飾者模式的一種實現方式。不過前面說的這些概念太乾了,我們用人話來翻譯一下,舉一個例子。
在日常開發寫bug
過程中,我們經常會用到防抖和節流,比如像下面這樣
class MyClass {
follow = debounce(function() {
console.log('我是子君,關注我哦')
}, 100)
}
const myClass = new MyClass()
// 多次呼叫只會輸出一次
myClass.follow()
myClass.follow()
上面是一個防抖的例子,我們通過debounce
函式將另一個函式包起來,實現了防抖的功能,這時候再有另一個需求,比如希望在呼叫follow
函式前後各列印一段日誌,這時候我們還可以再開發一個log
函式,然後繼續將follow
包裝起來
/**
* 最外層是防抖,否則log會被呼叫多次
*/
class MyClass {
follow = debounce(
log(function() {
console.log('我是子君,關注我哦')
}),
100
)
}
上面程式碼中的debounce
和log
兩個函式,本質上是兩個包裝函式,通過這兩個函式對原函式的包裝,使原函式的行為發生了變化,而js
中的裝飾器的原理就是這樣的,我們使用裝飾器對上面的程式碼進行改造
class MyClass {
@debounce(100)
@log
follow() {
console.log('我是子君,關注我哦')
}
}
裝飾器的形式就是 @ + 函式名
,如果有引數的話,後面的括號裡面可以傳參
在方法上使用裝飾器
裝飾器可以應用到class
上或者class
裡面的屬性上面,但一般情況下,應用到class
屬性上面的場景會比較多一些,比如像上面我們說的log
,debounce
等等,都一般會應用到類屬性上面,接下來我們一起來具體看一下如何實現一個裝飾器,並應用到類上面。在實現裝飾器之前,我們需要先了解一下屬性描述符
瞭解一下屬性描述符
在我們定義一個物件裡面的屬性的時候,其實這個屬性上面是有許多屬性描述符的,這些描述符標明瞭這個屬效能不能修改,能不能列舉,能不能刪除等等,同時ECMAScript
將這些屬性描述符分為兩類,分別是資料屬性和訪問器屬性,並且資料屬性與訪問器屬性是不能共存的。
資料屬性
資料屬性包含一個資料值的位置,在這個位置可以讀取和寫入值。資料屬性包含了四個描述符,分別是
configurable
表示能不能通過
delete
刪除屬性,能否修改屬性的其他描述符特性,或者能否將資料屬性修改為訪問器屬性。當我們通過let obj = {name: ''}
宣告一個物件的時候,這個物件裡面所有的屬性的configurable
描述符的值都是true
enumerable
表示能不能通過
for in
或者Object.keys
等方式獲取到屬性,我們一般宣告的物件裡面這個描述符的值是true
,但是對於class
類裡面的屬性來說,這個值是false
writable
表示能否修改屬性的資料值,通過將這個修改為
false
,可以實現屬性只讀的效果。value
表示當前屬性的資料值,讀取屬性值的時候,從這裡讀取;寫入屬性值的時候,會寫到這個位置。
訪問器屬性
訪問器屬性不包含資料值,他們包含了getter
與setter
兩個函式,同時configurable
與enumerable
是資料屬性與訪問器屬性共有的兩個描述符。
getter
在讀取屬性的時候呼叫這個函式,預設這個函式為
undefined
setter
在寫入屬性值的時候呼叫這個函式,預設這個函式為
undefined
瞭解了這六個描述符之後,你可能會有幾個疑問: 我如何去定義修改這些屬性描述符?這些屬性描述符與今天的文章主題有什麼關係?接下來是揭曉答案的時候了。
使用Object.defineProperty
瞭解過vue2.0
雙向繫結原理的同學一定知道,Vue
的雙向繫結就是通過使用Object.defineProperty
去定義資料屬性的getter
與setter
方法來實現的,比如下面有一個物件
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('我是子君,我的公眾號是 【前端有的玩】,關注有驚喜哦')
}
}
我們逐行去分析一下程式碼
- 首先我們定義了一個
debounce
函式,同時有一個引數wait
,這個函式對應的就是在下面呼叫裝飾器時使用的@debounce(100)
debounce
函式返回了一個新的函式,這個函式即裝飾器的核心,這個函式有三個引數,下面逐一分析target
: 這個類屬性函式是在誰上面掛載的,如上例對應的是MyClass
類name
: 這個類屬性函式的名稱,對應上面的follow
descriptor
: 這個就是我們前面說的屬性描述符,通過直接descriptor
上面的屬性,即可實現屬性只讀,資料重寫等功能
- 然後第三行
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. 函式節流與防抖
函式節流與防抖應用場景是比較廣的,一般使用時候會通過throttle
或debounce
方法對要呼叫的函式進行包裝,現在就可以使用上文說的內容將這兩個函式封裝成裝飾器, 防抖節流使用的是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
。
我是子君,今天就寫這麼多,本文首發於【前端有的玩】,這是一個專注於前端技術,前端面試相關的公眾號,同時關注之後即刻拉你加入前端交流群,我們一起聊前端,歡迎關注。
結語
不要吹滅你的靈感和你的想象力; 不要成為你的模型的奴隸。 ——文森特・梵高