高階組件(High Order Component,簡稱HOC)不是一個真的組件,而是一個沒有副作用的純函數,以組件作為參數,返回一個功能增強的新組件,在很多第三方庫(例如Redux、Relay等)中都有高階組件的身影。由于遵循了裝飾者模式的設計思想,因此不會入侵傳遞進來的原組件,而是對其進行抽象、包裝和拓展,改變原組件的行為(圖9形象的表達出了高階組件的作用)。這樣不僅增強了組件的復用性和靈活性,還保持了組件的易用性。靈活使用高階組件,可大大提高代碼質量。
:-: 
圖9 高階組件的作用
  高階組件有兩種常見的實現方式:代理和繼承,下面會分別做講解。
## 一、代理方式
  高階組件作為原組件的代理,不但會將其包裹住,還會給它添加新特性,并且提供了眾多控制原組件的功能,例如操縱props、抽取state、訪問實例和再包裝等。
**1)操縱props**
  在原組件(即被包裹組件)接收到props之前,高階組件可以將其攔截,執行增刪改操作,再將處理過的props傳給原組件。下面是一個簡單的示例,會在高階組件中新增了一個name屬性。
~~~js
//原組件
class Btn extends React.Component {
render() {
return <button>{this.props.name}</button>;
}
}
//高階組件
function HOC(Wrapped) {
class Enhanced extends React.Component {
constructor(props) {
super(props);
this.state = { name: "strick" };
}
render() {
return <Wrapped {...this.state} />;
}
}
return Enhanced;
}
const EnhancedBtn = HOC(Btn);
~~~
  HOC()函數就是高階組件,在函數體中聲明了用于修飾原組件Wrapped的新組件Enhanced,它的name狀態作為props傳給了Wrapped,并在render()方法中將Wrapped渲染出來。當執行HOC(Btn)后,就能得到增強了的EnhancedBtn組件。
**2)抽取state**
  將原組件的state和與之相關的處理函數抽取到高階組件中,從而使得原組件無狀態,變成容易復用的展示型組件。以一個能維護自己狀態的Input組件為例,如下所示。
~~~js
class Input extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
this.handle = this.handle.bind(this);
}
handle(e) {
this.setState({ value: e.target.value });
}
render() {
return (
<input type="text" value={this.state.value} onChange={this.handle} />
);
}
}
~~~
  現在將Input組件處理value狀態和onChange事件的代碼提升到高階組件中,如下代碼所示,在render()方法中初始化了一個newProps對象,用于把處理好的value狀態和事件處理程序handle()回傳給Input組件。
~~~js
function stateHOC(Wrapped) {
class Enhanced extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
this.handle = this.handle.bind(this);
}
handle(e) {
this.setState({ value: e.target.value });
}
render() {
let newProps = {
value: this.state.value,
onChange: this.handle
};
return <Wrapped {...newProps} />;
}
}
return Enhanced;
}
~~~
  經過高階組件的抽象后,Input組件就變得很簡單,如下代碼所示,沒有額外的邏輯操作,只要接收傳過來的props即可。
~~~js
class Input extends React.Component {
constructor(props) {
super(props);
}
render() {
return <input type="text" {...this.props} />;
}
}
~~~
**3)Refs**
  通過[第5篇](https://www.cnblogs.com/strick/p/10593862.html)提到的Refs訪問方式,可以得到被包裹組件的實例,從而就能操縱組件中的DOM元素。以下面的Btn組件為例,在高階組件中給它定義ref屬性,這樣就能在新組件的componentDidMount()方法中訪問到Btn組件的實例。
~~~js
class Btn extends React.Component {
render() {
return <button>提交</button>;
}
}
function refHOC(Wrapped) {
class Enhanced extends React.Component {
render() {
return <Wrapped ref={btn => { this.myBtn = btn }} />;
}
componentDidMount() {
console.log(this.myBtn); //Btn組件的實例
}
}
return Enhanced;
}
~~~
  注意,ref屬性不會傳遞給原組件,如果在上面的Btn組件中讀取this.props.ref,那么得到的值將是undefined,如下所示。
~~~js
class Btn extends React.Component {
render() {
console.log(this.props.ref); //undefined
}
}
~~~
**4)包裝**
  在高階組件中,還能通過引入其它React元素包裝原組件,既能改變布局,也能增加樣式。例如用一個包含內邊距的元素包裹原組件,并在其鄰近的位置新增一個文本框,如下所示。
~~~js
function wrappedHOC(Wrapped) {
class Enhanced extends React.Component {
render() {
return (
<div style={{ padding: 10 }}>
<input type="text" />
<Wrapped />
</div>
);
}
}
return Enhanced;
}
~~~
## 二、繼承方式
  繼承是另一種構建高階組件的方式,即新組件直接繼承原組件(如下代碼所示),從而實現通用邏輯的復用,并且還能使用原組件的state和props,以及生命周期等方法。
~~~js
function inheritHOC(Wrapped) {
class Enhanced extends Wrapped { }
return Enhanced;
}
~~~
  在代理方式下的新組件和原組件會各自經歷一次完整的生命周期,而在繼承方式下,兩者會共用一次生命周期。
**1)渲染劫持**
  在高階組件中,可以通過super.render()渲染原組件,從而就能控制高階組件的渲染結果,即渲染劫持。例如在新組件的render()方法中克隆原組件并為其傳遞新的props,如下所示。
~~~js
function inheritHOC(Wrapped) {
class Enhanced extends Wrapped {
render() {
//獲取原組件
const origin = super.render();
//合并原組件的屬性,并新增value屬性的值
const props = Object.assign({}, origin.props, {value: "strick"});
return React.cloneElement(origin, props, origin.props.children);
}
}
return Enhanced;
}
~~~
  代碼中的React.cloneElement()方法能接收3個參數,第一個是要克隆的React元素,后兩個是要傳遞的新props和原來的children屬性。
  除了render()方法,其余諸如componentWillMount()、componentWillUpdate()等生命周期中的方法也是能劫持的。
**2)使用state**
  在高階組件中,不僅可以讀取原組件的state,還能對其進行修改或增加,甚至是刪除。不過,這三類帶有侵略性的操作,會讓原組件內部變得混亂不堪,因此要慎用。在下面的示例中,Input組件包含一個value狀態,高階組件內的新組件Enhanced會在其構造函數中增加一個name狀態,并修改value狀態的值。
~~~js
class Input extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
}
render() {
return <input type="text" value={this.state.value} />;
}
}
function stateHOC(Wrapped) {
class Enhanced extends Wrapped {
constructor(props) {
super(props);
this.state.name = "strick"; //增加狀態
this.state.value = "init"; //修改狀態
}
render() {
return super.render();
}
}
return Enhanced;
}
let EnhancedInput = stateHOC(Input);
~~~
## 三、參數傳遞
  高階組件除了一個組件參數之外,還能接收其它類型的參數,例如為高階組件額外傳遞一個區分類別的type參數,如下所示。
~~~js
HOC(Wrapped, type)
~~~
  不過,在React中,函數式編程的參數傳遞更為常用,即使用柯里化的形式,如下代碼所示,其中HOC(type)會返回一個高階組件。
~~~js
HOC(type)(Wrapped)
~~~
  而在第三方庫中,這種形式的高階組件被大量應用,例如Redux中用于連接React組件與其Store的connect()函數,它是一個能返回高階組件的高階函數,其參數可以是兩個函數,如下所示。
~~~js
const Enhanced = connect(mapStateToProps, mapDispatchToProps)(Wrapped);
~~~
  將上面這條語句拆分成兩條目的更為清晰的語句,就能讓人更容易理解代碼的意圖,如下所示。
~~~js
const enhance = connect(mapStateToProps, mapDispatchToProps);
const Enhanced = enhance(Wrapped);
~~~
  雖然這種形式的高階組件會讓人困惑,但是更易于組合。因為它會把參數序列處理到只剩一個組件參數,而高階組件的返回值也是一個組件,也就是說,前一個高階組件的返回值可以作為后一個高階組件的參數,從而使得這些高階組件可以組合在一起。例如有三個高階組件f、g和h,它們可以像下面這樣組合在一起。
~~~js
f(g(h(Wrapped)))
~~~
  如果要嵌套的高階組件很多,那么這種寫法將變得異常丑陋且難以閱讀。這個時候,就可以引入compose()函數,它能將函數串聯起來,即用平鋪的寫法實現函數的組合,如下代碼所示,省略了compose()函數的具體實現。
~~~js
compose(f, g, h)
~~~
  compose()函數的執行方向是自右向左,并且還有一個限制,那就是第一個高階組件(即h)可以接收多個參數,但之后的就只能接收一個參數。
## 四、命名
  在高階組件中創建的新組件,不會再沿用原組件的名稱。為了便于在React Developer Tools中調試,需要為新組件設置一個顯示名稱,例如新組件的名稱是“Enhanced”,原組件的名稱是“Input”,那么就以“Enhanced(Input)”為顯示名稱。
  為了完成這個功能,高階組件可以修改成下面這樣。注意,在定義displayName屬性時,用到了ES6新增的模板字面量。
~~~js
function HOC(Wrapped) {
class Enhanced extends React.Component { }
Enhanced.displayName = `Enhanced(${getDisplayName(Wrapped)})`;
return Enhanced;
}
~~~
  getDisplayName()函數用于獲取組件的名稱(如下代碼所示),如果組件的displayName屬性不存在,那么就改用name屬性,即類或函數的名稱。
~~~js
function getDisplayName(Wrapped) {
return Wrapped.displayName || Wrapped.name || "Component";
}
~~~
## 五、注意事項
  (1)不要在組件的render()方法中使用高階組件。因為高階組件每次都會創建一個新組件,而根據React的diff算法可知,原組件(即前一次所創建的組件)會先被卸載掉,然后重新掛載新組件。這么做不僅性能低下,而且原組件的狀態和其所有子組件都將丟失。
  (2)高階組件創建的新組件不會包含原組件的靜態方法,如果需要,那么就得手動復制。
*****
> 原文出處:
[博客園-React躬行記](https://www.cnblogs.com/strick/category/1455720.html)
[知乎專欄-React躬行記](https://zhuanlan.zhihu.com/pwreact)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020