學習es7的Decorator(順帶寫個react高階元件)
在很多框架和庫中看到它的身影,尤其是React和Redux,還有mobx中,那什麼是裝飾器呢。
修飾器(Decorator)是一個函式,用來修改類的行為。不是很理解這種抽象概念,還是看程式碼講解實際些。
//定義一個函式,也就是定義一個Decorator,target引數就是傳進來的Class。 //這裡是為類添加了一個靜態屬性 function addAge(target) { target.age = 2; } //在Decorator後面跟著Class,Decorator是函式的話,怎麼不是addAge(MyGeekjcProject)這樣寫呢? //我只能這樣理解:因為語法就這樣,只要Decorator後面是Class,預設就已經把Class當成引數隱形傳進Decorator了(就是所謂的語法糖)。 @addAge class MyGeekjcProject {} console.log(MyGeekjcProject.age) // 2 複製程式碼
1.1 傳參修飾類
但上面這樣只傳了一個Class作為引數不夠靈活怎麼辦? 我們可以在外層套一個函式,只要最後返回的是一個Decorator即可,管你套多少個函式傳多少個引數都無所謂。
function addAge(age) { return function(target) { target.age = age; } } //注意這裡,隱形傳入了Class,語法類似於addAge(2)(MyGeekjcProject) @testable(2) class MyGeekjcProject {} MyGeekjcProject.age // 2 @addAge(3) class MyGeekjcProject {} MyGeekjcProject.age // 3 複製程式碼
1.2 修飾類的屬性
下面是修改類的prototype物件
function description(target) { target.prototype.url = 'https://www.geekjc.com'; } @description class MyGeekjcProject {} let geekjc = new MyGeekjcProject(); geekjc.url // https://www.geekjc.com 複製程式碼
概念大概理解了,修飾器不僅可以修飾類,還可以修飾類的屬性
//假如修飾類的屬性則傳入三個引數,對應Object.defineProperty()裡三個引數,具體不細說 //target為目標物件,對應為Class的例項 //name為所要修飾的屬性名,這裡就是修飾器緊跟其後的name屬性 //descriptor為該屬性的描述物件 //這裡的Decorator作用是使name屬性不可寫,並返回修改後的descriptor function readonly(target, name, descriptor){ descriptor.writable = false; return descriptor; } class Person { @readonly name() { return `${this.first} ${this.last}` } } 複製程式碼
關於Object.defineProperty()可以看文章學習 ofollow,noindex">Object.defineProperty
再看一個複雜的例子
//定義一個Class並在其add上使用了修飾器 class Math { @log add(a, b) { return a + b; } } //定義一個修飾器 function log(target, name, descriptor) { //這裡是快取舊的方法,也就是上面那個add()原始方法 var oldValue = descriptor.value; //這裡修改了方法,使其作用變成一個列印函式 //最後依舊返回舊的方法,真是巧妙 descriptor.value = function() { console.log(`Calling "${name}" with`, arguments); return oldValue.apply(null, arguments); }; return descriptor; } const math = new Math(); math.add(2, 4); 複製程式碼
看完上面的程式碼就知道decorator有什麼用了,修飾器不僅可以修飾類的行為,還可以修飾類的屬性。
2. react高階元件
更通俗地描述為,高階元件通過包裹(wrapped)被傳入的React元件,經過一系列處理,最終返回一個相對增強(enhanced)的React元件,供其他元件呼叫。
3. 實現一個高階元件
面我們來實現一個最簡單的高階元件(函式),它接受一個React元件,包裹後然後返回。
export default function withDescription(WrappedComponent) { return class HOC extends Component { render() { return <div> <div className="geekjc-description"> 極客教程(https://www.geekjc.com)致力於推廣各種程式語言技術,也為了未來數字化世界,讓人更容易找到運算元字化的方式,為了未來而生的程式設計學習平臺, 大部分資源是完全免費的,並且會根據當前網際網路的變化實時更新本站內容。 </div> <WrappedComponent {...this.props}/> </div> } } } 複製程式碼
在其他元件裡,我們引用這個高階元件,用來強化它。
@withDescription export default class Geekjc extends Component { render() { return ( <div> 我是一個普通元件 </div> ); } } 複製程式碼
在這裡使用了 ES7
裡的 decorator
(可以看第一節的Decorator),來提升寫法上的優雅,但是實際上它只是一個語法糖,下面這種寫法也是可以的。
const EnhanceDemo = withDescription(Geekjc); 複製程式碼
隨後,觀察React元件樹發生了什麼變化,如圖所示,可以發現Geekjc元件被HOC元件包裹起來了,符合了高階元件的預期,即元件是層層包裹起來的,如同洋蔥一樣。

但是隨之帶來的問題是,如果這個高階元件被使用了多次,那麼在除錯的時候,將會看到一大堆 HOC
,所以這個時候需要做一點小優化,就是在高階元件包裹後,應當保留其原有名稱。
我們改寫一下上述的高階元件程式碼,增加了 getDisplayName
函式以及靜態屬性 displayName
,此時再去觀察 DOM Tree
。
function getDisplayName(component) { return component.displayName || component.name || 'Component'; } export default function (WrappedComponent) { return class HOC extends Component { static displayName = `HOC(${getDisplayName(WrappedComponent)})` render() { return <div> <div className="geekjc-description"> 極客教程(https://www.geekjc.com)致力於推廣各種程式語言技術,也為了未來數字化世界,讓人更容易找到運算元字化的方式,為了未來而生的程式設計學習平臺, 大部分資源是完全免費的,並且會根據當前網際網路的變化實時更新本站內容。 </div> <WrappedComponent {...this.props}/> </div> } } } 複製程式碼

此時,原本元件的名稱正確地顯示在了 DOM Tree
上。
這個簡單的例子裡高階元件只做了一件事,那便是 為被包裹的元件新增一個標題樣式 。這個高階元件可以用到任何一個需要新增此邏輯的元件上,只需要被此高階元件修飾即可。
由此可以看出,高階元件的主要功能是封裝並抽離元件的通用邏輯,讓此部分邏輯在元件間更好地被複用。
4. 高階元件的進階用法
4.1 元件引數
還是以上述例子為例,此高階元件僅僅只是展示了關於網站的描述,但是為了更好的抽象,此描述應當可以被引數化,如下方式呼叫。
@withDescription('歡迎大家訪問收藏(https://www.geekjc.com)') export default class Geekjc extends Component { render() { return ( //... ); } } 複製程式碼
withDescription需要被改寫成如下形式,它接受一個引數,然後返回一個高階元件(函式)。
export default function (description) { return function (WrappedComponent) { return class HOC extends Component { render() { return <div> <div className="geekjc-description"> {description ?description : '我是描述'} </div> <WrappedComponent {...this.props}/> </div> } } } } 複製程式碼
使用ES6寫法可以更加簡潔。
export default(description) => (WrappedComponent) => class HOC extends Component { render() { return <div> <div className="geekjc-description"> {description ? description : '我是描述'} </div> <WrappedComponent {...this.props}/> </div> } } 複製程式碼
如圖可以看到,傳入的引數已經反映在DOM Tree裡了。

柯里化 Curry 概念:只傳遞函式的一部分引數來呼叫它,讓它返回一個函式去處理剩下的引數。 函式簽名:fun(params)(otherParams) 應用:在React裡,通過柯里化,我們可以通過傳入不同的引數來得到不同的高階元件。
4.2 基於屬性代理的方式
屬性代理是最常見的高階元件的使用方式,上述描述的高階元件就是這種方式。它通過做一些操作,將被包裹元件的props和新生成的props一起傳遞給此元件,這稱之為屬性代理。
export default function withHeader(WrappedComponent) { return class HOC extends Component { render() { const newProps = { test:'hoc' } // 透傳props,並且傳遞新的newProps return <div> <WrappedComponent {...this.props} {...newProps}/> </div> } } } 複製程式碼
4.3 基於反向繼承的方式
這種方式返回的React元件繼承了被傳入的元件,所以它能夠訪問到的區域、許可權更多,相比屬性代理方式,它更像打入組織內部,對其進行修改。具體的可以參考附錄裡提供的連結進行深入學習。
export default function (WrappedComponent) { return class Inheritance extends WrappedComponent { componentDidMount() { // 可以方便地得到state,做一些更深入的修改。 console.log(this.state); } render() { return super.render(); } } } 複製程式碼
4.4 組合多個高階元件
上述高階元件為React元件增強了一個功能,如果需要同時增加多個功能需要怎麼做?這種場景非常常見,例如我既需要增加一個元件標題,又需要在此元件未載入完成時顯示Loading。
@withDescription @withLoading class Demo extends Component{ } 複製程式碼
使用compose可以簡化上述過程,也能體現函數語言程式設計的思想。
const enhance = compose(withHeader,withLoading); @enhance class Demo extends Component{ } 複製程式碼
組合 Compose compose可以幫助我們組合任意個(包括0個)高階函式,例如compose(a,b,c)返回一個新的函式d,函式d依然接受一個函式作為入參,只不過在內部會依次呼叫c,b,a,從表現層對使用者保持透明。 基於這個特性,我們便可以非常便捷地為某個元件增強或減弱其特徵,只需要去變更compose函式裡的引數個數便可。 compose函式實現方式有很多種,這裡推薦其中一個recompact.compose,詳情見下方參考類庫,也可以看我之前寫的一篇文章 reduce與redux中compose函式
5. 與父元件區別
高階元件作為一個函式,它可以更加純粹地關注業務邏輯層面的程式碼,比如資料處理,資料校驗,傳送請求等,可以改善目前程式碼裡業務邏輯和UI邏輯混雜在一起的現狀。父元件則是UI層的東西,我們先前經常把一些業務邏輯處理放在父元件裡,這樣會造成父元件混亂的情況。為了程式碼進一步解耦,可以考慮使用高階元件這種模式。
6. 開源的高階元件使用
6.1 recompact
recompact提供了一系列使用的高階元件,可以增強元件的行為,可以利用此庫學習高階元件的寫法。
import recompact from 'recompact' import { pure, withProps } from 'recompact' const enhance = recompact.compose( withProps({ className: 'beautiful' }), pure, ) @enhance class Demo extends Component{ } 複製程式碼