原文: https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3ejavascript
本文面向想要探索 HOC 模式的進階用戶,若是你是 React 的初學者則應該從官方文檔開始。高階組件(Higher Order Components)是一種很棒的模式,已被不少 React 庫證明是很是有價值的。在本文中,咱們首先回顧一下 HOC 是什麼、有什麼用、有何侷限,以及是如何實現它的。css
在附錄中,檢視了相關的話題,這些話題並不是 HOC 的核心,但我認爲應該說起。html
本文旨在儘可能詳細的論述,以便於讀者查閱;並假定你已經知曉 ES6。java
走你!react
高階組件就是包裹了其餘 React Component 的組件git
一般,這個模式被實現爲一個函數,基本算是個類工廠方法(yes, a class factory!),其函數簽名用 haskell 風格的僞代碼寫出來就是這樣的:github
hocFactory:: W: React.Component => E: React.Component
複製代碼
W (WrappedComponent) 是被包裹的 React.Component;而函數返回的 E (Enhanced Component) 則是新獲得的 HOC,也是個 React.Component。bash
定義中的「包裹」是一種有意的模糊,意味着兩件事情:app
後面會詳述這兩種模式的。函數
在大的維度上 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
能夠對傳遞給被包裹組件的屬性進行增刪查改。但刪除或編輯重要屬性時要謹慎,應合理設置 HOC 的命名空間以避免影響被包裹組件。
例子:增長新屬性。應用中經過 this.props.user 將能夠獲得已登陸用戶
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
}
return <WrappedComponent {...this.props} {...newProps}/> } } } 複製代碼
能夠經過 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,這很是相似於 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 劫持」是由於 HOC 控制了被包裹組件的 render 輸出,並能對其作任何事情。
在 render 劫持中能夠:
*用 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 來處理實例。
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)已經提供了。
如下爲能夠跳過的選讀內容
在 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{}
複製代碼
如下爲能夠跳過的選讀內容
有一些子組件的 React 組件稱爲父組件,React 有一些訪問和控制組件子成員的 API。
例子:父組件訪問子組件
class Parent extends React.Component {
render() {
return (
<div>
{this.props.children}
</div>
)
}
}
}
render((
<Parent>
{children}
</Parent>
), mountNode)
複製代碼
相比於 HOC,來細數一下父組件能作和不能作的:
一般,父組件的作法沒有 HOC 那麼 hacky,但上述列表是其相比於 HOC 的不靈活之處。
但願閱讀本文後你能對 React HOC 多一些瞭解。在不一樣的庫中,HOC 都被證實是頗有價值並不是常好用的。
React 帶來了不少創新,人們普遍應用着 Radium、React-Redux、React-Router 等等,也很好的印證了這一點。
長按二維碼或搜索 fewelife 關注咱們哦