原文: https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e
概要
本文面向想要探索 HOC 模式的進階使用者,如果你是 React 的初學者則應該從官方文件開始。高階元件(Higher Order Components)是一種很棒的模式,已被很多 React 庫證實是非常有價值的。在本文中,我們首先回顧一下 HOC 是什麼、有什麼用、有何侷限,以及是如何實現它的。
在附錄中,檢視了相關的話題,這些話題並非 HOC 的核心,但我認為應該提及。
本文旨在儘量詳細的論述,以便於讀者查閱;並假定你已經知曉 ES6。
走你!
高階元件是什麼?
高階元件就是包裹了其他 React Component 的元件
通常,這個模式被實現為一個函式,基本算是個類工廠方法(yes, a class factory!),其函式簽名用 haskell 風格的虛擬碼寫出來就是這樣的:
hocFactory:: W: React.Component => E: React.Component
複製程式碼
W (WrappedComponent) 是被包裹的 React.Component;而函式返回的 E (Enhanced Component) 則是新得到的 HOC,也是個 React.Component。
定義中的“包裹”是一種有意的模糊,意味著兩件事情:
- 屬性代理:由 HOC 操縱那些被傳遞給被包裹元件 W 的 props
- 繼承反轉:HOC 繼承被包裹元件 W
後面會詳述這兩種模式的。
HOC 能做什麼?
在大的維度上 HOC 能用於:
- 程式碼重用和邏輯抽象
- render 劫持
- state 抽象和操縱
- 操縱屬性(props)
後面將會看到這些類目的細節,但首先來學習一下實現 HOC 的方式,因為實現方式決定了 HOC 實際能做的事情。
HOC 工廠實現
屬性代理(PP)和繼承反轉(II)。兩者皆提供了不同的途徑以操縱被包裹的元件。
屬性代理
屬性代理(Props Proxy)可以用以下方式簡單的實現:
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}
複製程式碼
此處關鍵的部分在於 HOC 的 render() 方法返回了一個被包裹元件的 React Element。同時,將 HOC 接受到的屬性傳遞給了被包裹的元件,因此稱為**“屬性代理”**。
注意:
<WrappedComponent {...this.props}/>
// 等價於
React.createElement(WrappedComponent, this.props, null)
複製程式碼
兩者都會建立一個 React Element,用於描述 React 在其一致性比較過程中應該渲染什麼。
瞭解更多:
關於 React Elment vs Components 的內容可以檢視
https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html
一致性比較過程
http://www.css88.com/react/docs/reconciliation.html
可以用屬性代理做些什麼?
- 操縱屬性
- 通過 refs 訪問例項
- 抽象 state
- 包裹元件
操縱屬性
可以對傳遞給被包裹元件的屬性進行增刪查改。但刪除或編輯重要屬性時要謹慎,應合理設定 HOC 的名稱空間以免影響被包裹元件。
例子:增加新屬性。應用中通過 this.props.user 將可以得到已登入使用者
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
複製程式碼
通過 refs 訪問例項
可以通過 ref
訪問到 this
(被包裹元件的例項),但這需要 ref
所引用的被包裹元件執行一次完整的初始化 render 過程,這就意味著要從 HOC 的 render 方法中返回被包裹元件的元素,並讓 React 完成其一致性比較過程,而 ref
能引用該元件的例項就好了。
例子:下例中展示瞭如何通過 refs 訪問到被包裹元件的例項方法和例項本身
function refsHOC(WrappedComponent) {
return class RefsHOC extends React.Component {
proc(wrappedComponentInstance) {
wrappedComponentInstance.method()
}
render() {
const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
return <WrappedComponent {...props}/>
}
}
}
複製程式碼
當被包裹元件被渲染,ref 回撥就將執行,由此就能獲得其例項的引用。這可以用於讀取、增加例項屬性,或呼叫例項方法。
抽象 state
通過提供給被包裹元件的屬性和回撥,可以抽象 state,這非常類似於 smart 元件是如何處理 dumb 元件的。
關於上述兩種元件可以參閱:
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
例子:在下面這個抽象 state 的例子裡我們簡單的將 value 和 onChange 處理函式從 name 輸入框中抽象出來。之所以說“簡單”是因為這非常普遍,但你必須明白這一點。
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
constructor(props) {
super(props)
this.state = {
name: ''
}
this.onNameChange = this.onNameChange.bind(this)
}
onNameChange(event) {
this.setState({
name: event.target.value
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onNameChange
}
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
複製程式碼
用起來可能會是這樣的:
@ppHOC
class Example extends React.Component {
render() {
return <input name="name" {...this.props.name}/>
}
}
複製程式碼
於是這個輸入框就自動成為了一個受控元件。
關於受控元件:
https://mp.weixin.qq.com/s/I3aPxyZA_iArUDmsXtXGcw
包裹元件
可以利用元件的包裹,實現樣式定義、佈局或其他目標。一些基礎用法可以由普通的父元件完成(參閱附錄B),但如前所述,用 HOC 可以更加靈活。
例子:為定義樣式而實現的包裹
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return (
<div style={{display: 'block'}}>
<WrappedComponent {...this.props}/>
</div>
)
}
}
}
複製程式碼
繼承反轉
繼承反轉 (Inheritance Inversion) 只需要這樣實現就可以:
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
return super.render()
}
}
}
複製程式碼
如你所見,被返回的 HOC 類(強化過的類)繼承了被包裹的元件。之所以被稱為“繼承反轉”是因為,被包裹元件並不去繼承強化類,而是被動的讓強化類繼承。通過這種方式,兩個類的關係看起來反轉了。
繼承反轉使得 HOC 可以用 this
訪問被包裹元件的例項,這意味著可以訪問 state、props、元件生命週期鉤子,以及 render 方法。
這裡並不深入探討可以在生命週期鉤子中實現的細節,因為那屬於 React 的範疇。但要知道通過繼承反轉可以為被包裹元件建立新的生命週期鉤子;並記住總是應該呼叫 super.[lifecycleHook]
以確保不會破壞被包裹的元件。
一致性比較過程
在深入之前我們大概說一下這些理論。
一致性比較
https://facebook.github.io/react/docs/reconciliation.html
React Elements 描述了 React 執行其一致性比較過程時,什麼會被渲染。
React Elements 可以是兩種型別:字串和函式。字串型別的 React Elements(STRE)代表 DOM 節點,函式型別的 React Elements(FTRE)代表繼承自 React.Component 的元件。
React 元素和元件
https://facebook.github.io/react/blog/2015/12/18/react-components-elements-and-instances.html
在 React 的一致性比較過程(最終結果是 DOM 元素)中,FTRE 會被處理成一棵完整的 STRE 樹。
之所以很重要,就在於這意味著繼承反轉高階元件並不保證處理完整的子樹。
後面學習到 render 劫持的時候將會證明其重要性。
可以用繼承反轉做些什麼?
- render 劫持
- 操縱 state
render 劫持
稱之為“render 劫持”是因為 HOC 控制了被包裹元件的 render 輸出,並能對其做任何事情。
在 render 劫持中可以:
- 在任何 render 輸出的 React Elements 上增刪查改 props
- 讀取並修改 render 輸出的 React Elements 樹
- 條件性顯示元素樹
- 出於定製樣式的目的包裹元素樹(正如屬性代理中展示的)
*用 render 引用被包裹元件的 render 方法
不能對被包裹元件的例項編輯或建立屬性,因為一個 React Component 無法編輯其收到的 props,但可以改變被 render 方法輸出的元素的屬性。
就如我們之前學到的,繼承反轉 HOC 不保證處理完整的子樹,這意味著 render 劫持技術有一些限制。經驗法則是,藉助於 render 劫持,可以不多不少的操作被包裹元件的 render 方法輸出的元素樹。如果那個元素數包含了一個函式型別的 React Component,那就無法操作其子元件(被 React 的一致性比較過程延遲到真正渲染到螢幕上時)。
例子1:條件性渲染
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render()
} else {
return null
}
}
}
}
複製程式碼
例子2:修改 render 輸出的元素樹
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
const elementsTree = super.render()
let newProps = {};
if (elementsTree && elementsTree.type === 'input') {
newProps = {value: 'may the force be with you'}
}
const props = Object.assign({}, elementsTree.props, newProps)
const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children)
return newElementsTree
}
}
}
複製程式碼
本例中,如果由 render 輸出的被包裹元件有一個 input 頂級元素,就改變其 value。
可以在這裡做任何事情,可以遍歷整個元素樹並改變其中的任何一個元素屬性。
注意:不能通過屬性代理劫持 render
雖然通過 WrappedComponent.prototype.render 訪問 render 方法是可能的,但這樣一來你就要模擬被包裹元件的例項及其屬性,並自己處理元件生命週期而非依靠 React 去解決。以我的經驗來說這是得不償失的,如果要劫持 render 應該用繼承反轉而非屬性代理。要記住 React 內在地處置元件例項,而你只能通過
this
或 refs 來處理例項。
操縱 state
HOC 可以讀取、編輯和刪除被包裹元件例項的 state,也可以按需增加更多的 state。要謹記如果把 state 搞亂會很糟糕。大部分 HOC 應該限制讀取或增加 state,而後者(譯註:增加 state)應該使用名稱空間以免和被包裹元件的 state 搞混。
例子:對訪問被包裹元件的 props 和 state 的除錯
export function IIHOCDEBUGGER(WrappedComponent) {
return class II extends WrappedComponent {
render() {
return (
<div>
<h2>HOC Debugger Component</h2>
<p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre>
<p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
{super.render()}
</div>
)
}
}
}
複製程式碼
該 HOC 將被包裹元件嵌入其他元素中,並顯示了其 props 和 state。
命名
使用 HOC 時,就失去了被包裹元件原有的名字,可能會影響開發和除錯。
人們通常的做法就是用原有名字加上些什麼來命名 HOC。下面的例子取自 React-Redux
HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`
//or
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
...
}
複製程式碼
而 getDisplayName 函式的定義如下:
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
‘Component’
}
複製程式碼
其實你都不需要自己寫一遍這個函式,recompose 庫(https://github.com/acdlite/recompose)已經提供了。
附錄 A:HOC 和引數
以下為可以跳過的選讀內容
在 HOC 中可以善用引數。這本來已經在上面所有例子中隱含的出現過,並且對於中級 JS 開發者也已經稀鬆平常了,但是本著知無不言的原則,還是快速過一遍吧。
例子:結合屬性代理和 HOC 引數,需要關注的是 HOCFactoryFactory 函式
function HOCFactoryFactory(...params){
// do something with params
return function HOCFactory(WrappedComponent) {
return class HOC extends React.Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}
}
複製程式碼
可以這樣使用:
HOCFactoryFactory(params)(WrappedComponent)
//or
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component{}
複製程式碼
附錄 B:和父元件的區別
以下為可以跳過的選讀內容
有一些子元件的 React 元件稱為父元件,React 有一些訪問和控制元件子成員的 API。
例子:父元件訪問子元件
class Parent extends React.Component {
render() {
return (
<div>
{this.props.children}
</div>
)
}
}
}
render((
<Parent>
{children}
</Parent>
), mountNode)
複製程式碼
相比於 HOC,來細數一下父元件能做和不能做的:
- render 劫持(在繼承反轉中看見過)
- 控制內部 props(同樣在繼承反轉中看見過)
- 抽象 state,但存在缺點。將無法在外部訪問父元素的 state,除非特意為止建立鉤子。這限制了其實用性
- 包裹新的 React Elements。這可能是父元件唯一強於 HOC 的用例,雖然 HOC 也能做到
- 操縱子元件有一些陷阱。比如說如果 children 的根一級並不只有單一的子元件(多於一個的第一級子元件),你就得新增一個額外的元素來收納所有子元素,這會讓你的程式碼有些冗繁。在 HOC 中一個單一的頂級子元件是被 React/JSX 的約束所保證的。
通常,父元件的做法沒有 HOC 那麼 hacky,但上述列表是其相比於 HOC 的不靈活之處。
結語
希望閱讀本文後你能對 React HOC 多一些瞭解。在不同的庫中,HOC 都被證明是很有價值並非常好用的。
React 帶來了很多創新,人們廣泛應用著 Radium、React-Redux、React-Router 等等,也很好的印證了這一點。
長按二維碼或搜尋 fewelife 關注我們哦