在目前的前端社區(qū),『推崇組合湿蛔,不推薦繼承(prefer composition than inheritance)』已經(jīng)成為了比較好的實踐怎燥,mixin 也因為自身的一些問題而漸漸不被推薦。高階組件(Higher order components)作為 mixin 之外的一種組件抽象與處理形式呼股,有哪些不同和好處呢?繼續(xù)閱讀來了解一下吧画恰!
本文翻譯自 franleplant 的博客 React Higher Order Components in depth (閱讀原文及文中部分鏈接請自備梯子)
摘要
這篇文章的目標(biāo)群體是有一定基礎(chǔ)的并想要了解高階組件的用戶彭谁。如果你是 React 新手,那么可能你應(yīng)該先閱讀 React 的文檔允扇。
高階組件是一個非常棒的形式缠局,它已經(jīng)在多個 React 庫中被證明了它的價值。這篇文章中考润,我們將回顧什么是高階組件狭园,如何使用高階組件,它的限制以及如何編寫高階組件糊治。
附錄中我們回顧了與高階組件相關(guān)的話題(非核心)唱矛,但是是我認(rèn)為應(yīng)當(dāng)涉及到的知識。
這篇文章意在寫的詳盡井辜,如果你發(fā)現(xiàn)了任何遺漏绎谦,請你報告它,我會做出相應(yīng)的改變粥脚。
這篇文章假設(shè)讀者擁有 ES6 的相關(guān)知識窃肠。
讓我們開始吧!
什么是高階組件刷允?
一個高階組件只是一個包裝了另外一個 React 組件的 React 組件冤留。
這種形式通常實現(xiàn)為一個函數(shù),本質(zhì)上是一個類工廠(class factory)树灶,它下方的函數(shù)標(biāo)簽偽代碼啟發(fā)自 Haskell
hocFactory:: W: React.Component => E: React.Component
這里 W(WrappedComponent) 指被包裝的 React.Component纤怒,E(Enhanced Component) 指返回的新的高階 React 組件。
定義中的『包裝』一詞故意被定義的比較模糊破托,因為它可以指兩件事情:
- 屬性代理(Props Proxy):高階組件操控傳遞給 WrappedComponent 的 props肪跋,
- 反向繼承(Inheritance Inversion):高階組件繼承(extends)WrappedComponent。
我們將討論這兩種形式的更多細(xì)節(jié)土砂。
我可以使用高階組件做什么呢州既?
概括的講谜洽,高階組件允許你做:
- 代碼復(fù)用,邏輯抽象吴叶,抽離底層準(zhǔn)備(bootstrap)代碼
- 渲染劫持
- State 抽象和更改
- Props 更改
在探討這些東西的細(xì)節(jié)之前阐虚,我們先學(xué)習(xí)如何實現(xiàn)一個高階組件,因為實現(xiàn)方式『允許/限制』你可以通過高階組件做哪些事情蚌卤。
高階組件工廠的實現(xiàn)
在這節(jié)中我們將學(xué)習(xí)兩種主流的在 React 中實現(xiàn)高階組件的方法:屬性代理(Props Proxy)和 反向繼承(Inheritance Inversion)实束。兩種方法囊括了幾種包裝 WrappedComponent 的方法。
Props Proxy (PP)
屬性代理的實現(xiàn)方法如下:
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}
可以看到逊彭,這里高階組件的 render 方法返回了一個 type 為 WrappedComponent 的 React Element(也就是被包裝的那個組件)咸灿,我們把高階組件收到的 props 傳遞給它,因此得名 Props Proxy侮叮。
注意:
<WrappedComponent {...this.props}/>
// is equivalent to
React.createElement(WrappedComponent, this.props, null)
它們都創(chuàng)建了一個 React Element避矢,描述了 React 在『reconciliation』(可以理解為解析)階段的渲染內(nèi)容,如果你想了解更多關(guān)于 React Element 的內(nèi)容囊榜,請看 Dan Abramov 的這篇博客 和官方文檔上關(guān)于 reconciliation process 的部分审胸。
Props Proxy 可以做什么?
- 更改 props
- 通過 refs 獲取組件實例
- 抽象 state
- 把 WrappedComponent 與其它 elements 包裝在一起
更改 props
你可以『讀取卸勺,添加砂沛,修改,刪除』將要傳遞給 WrappedComponent 的 props曙求。
在修改或刪除重要 props 的時候要小心碍庵,你可能應(yīng)該給高階組件的 props 指定命名空間(namespace),以防破壞從外傳遞給 WrappedComponent 的 props圆到。
例子:添加新 props怎抛。這個應(yīng)用目前登陸的一個用戶可以在 WrappedComponent 通過 this.props.user 獲取
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
const newProps = {
user: currentLoggedInUser
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
通過 refs 獲取組件實例
你可以通過 ref 獲取關(guān)鍵詞 this(WrappedComponent 的實例),但是想要它生效芽淡,必須先經(jīng)歷一次正常的渲染過程來讓 ref 得到計算马绝,這意味著你需要在高階組件的 render 方法中返回 WrappedComponent,讓 React 進(jìn)行 reconciliation 過程挣菲,這之后你就通過 ref 獲取到這個 WrappedComponent 的實例了富稻。
例子:下方例子中,我們實現(xiàn)了通過 ref 獲取 WrappedComponent 實例并調(diào)用實例方法白胀。
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}/>
}
}
}
當(dāng) WrappedComponent 被渲染后椭赋,ref 上的回調(diào)函數(shù) proc 將會執(zhí)行,此時就有了這個 WrappedComponent 的實例的引用或杠。這個可以用來『讀取哪怔,添加』實例的 props 或用來執(zhí)行實例方法。
抽象 state
你可以通過向 WrappedComponent 傳遞 props 和 callbacks(回調(diào)函數(shù))來抽象 state,這和 React 中另外一個組件構(gòu)成思想 Presentational and Container Components 很相似认境。
例子:在下面這個抽象 state 的例子中胚委,我們幼稚地(原話是naively :D)抽象出了 name input 的 value 和 onChange。我說這是幼稚的是因為這樣寫并不常見叉信,但是你會理解到點亩冬。
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}/>
}
}
這里的 input 自動成為一個受控的 input。
點擊此鏈接查看一個更常見的雙向綁定的高階組件例子
把 WrappedComponent 與其它 elements 包裝在一起
出于操作樣式硼身、布局或其它目的硅急,你可以將 WrappedComponent 與其它組件包裝在一起。一些基本的用法也可以使用正常的父組件來實現(xiàn)(附錄 B)佳遂,但是就像之前所描述的营袜,使用高階組件你可以獲得更多的靈活性。
例子:包裝來操作樣式
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return (
<div style={{display: 'block'}}>
<WrappedComponent {...this.props}/>
</div>
)
}
}
}
Inheritance Inversion(II)
反向繼承(II)可以像這樣簡單地實現(xiàn):
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
return super.render()
}
}
}
如你所見讶迁,返回的高階組件類(Enhancer)繼承了 WrappedComponent连茧。這被叫做反向繼承是因為 WrappedComponent 被動地被 Enhancer 繼承,而不是 WrappedComponent 去繼承 Enhancer巍糯。通過這種方式他們之間的關(guān)系倒轉(zhuǎn)了。
反向繼承允許高階組件通過 this 關(guān)鍵詞獲取 WrappedComponent客扎,意味著它可以獲取到 state祟峦,props,組件生命周期(component lifecycle)鉤子徙鱼,以及渲染方法(render)宅楞。
我不會詳細(xì)介紹你可以使用組件生命周期方法做什么,因為這是 React 的內(nèi)容袱吆,而不是高階組件的厌衙。但是請注意,你可以通過高階組件來給 WrappedComponent 創(chuàng)建新的生命周期掛鉤方法绞绒,別忘了調(diào)用 super.[lifecycleHook] 防止破壞 WrappedComponent婶希。
Reconciliation 過程
介紹之前先來總結(jié)一些理論。
React Element 在 React 執(zhí)行它的 reconciliation 的過程時描述什么將被渲染蓬衡。
React Element 可以是兩個種類其中的一種:String 或 Function喻杈。String 類型的 React Element 代表原聲 DOM 節(jié)點,F(xiàn)unction 類型的 React Element 代表通過 React.Component 創(chuàng)建的組件狰晚。想要了解更多關(guān)于 Elements 和 Components 的知識請閱讀此推文筒饰。
Function 類型的 React Element 將在 reconciliation 階段被解析成 DOM 類型的 React Element (最終結(jié)果一定都是 DOM 元素)。
這點非常重要壁晒,這意味著『反向繼承的高階組件不保證一定解析整個子元素樹』瓷们。這對渲染劫持非常重要。
可以用反向繼承高階組件做什么?
- 渲染劫持(Render Highjacking)
- 操作 state
渲染劫持
它被叫做渲染劫持是因為高階組件控制了 WrappedComponent 生成的渲染結(jié)果谬晕,并且可以做各種操作式镐。
通過渲染劫持你可以:
- 『讀取、添加固蚤、修改娘汞、刪除』任何一個將被渲染的 React Element 的 props
- 在渲染方法中讀取或更改 React Elements tree,也就是 WrappedComponent 的 children
- 根據(jù)條件不同夕玩,選擇性的渲染子樹
- 給子樹里的元素變更樣式
*渲染 指的是 WrappedComponent.render 方法
你無法更改或創(chuàng)建 props 給 WrappedComponent 實例你弦,因為 React 不允許變更一個組件收到的 props,但是你可以在 render 方法里更改子元素/子組件們的 props燎孟。
就像之前所說的禽作,反向繼承的高階組件不能保證一定渲染整個子元素樹,這同時也給渲染劫持增添了一些限制揩页。通過反向繼承旷偿,你只能劫持 WrappedComponent 渲染的元素,這意味著如果 WrappedComponent 的子元素里有 Function 類型的 React Element爆侣,你不能劫持這個元素里面的子元素樹的渲染萍程。
例子1:條件性渲染。如果 this.props.loggedIn 是 true兔仰,這個高階組件會原封不動地渲染 WrappedComponent茫负,如果不是 true 則不渲染(假設(shè)此組件會收到 loggedIn 的 prop)
function iiHOC(WrappedComponent) {
return class Enhancer extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render()
} else {
return null
}
}
}
}
例子2:通過 render 來變成 React Elements tree 的結(jié)果
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
}
}
}
在這個例子中,如果 WrappedComponent 的頂層元素是一個 input乎赴,則改變它的值為 “may the force be with you”忍法。
這里你可以做任何操作,比如你可以遍歷整個 element tree 然后變更某些元素的 props榕吼。這恰好就是 Radium 的工作方式饿序。
注意:你不能通過 Props Proxy 來做渲染劫持
即使你可以通過 WrappedComponent.prototype.render 獲取它的 render 方法,你需要自己手動模擬整個實例以及生命周期方法羹蚣,而不是依靠 React原探,這是不值當(dāng)?shù)模瑧?yīng)該使用反向繼承來做到渲染劫持度宦。要記住 React 在內(nèi)部處理組件的實例踢匣,而你只通過 this 或 refs 來處理實例。
操作 state
高階組件可以 『讀取戈抄、修改离唬、刪除』WrappedComponent 實例的 state,如果需要也可以添加新的 state划鸽。需要記住的是输莺,你在弄亂 WrappedComponent 的 state戚哎,可能會導(dǎo)致破壞一些東西。通常不建議使用高階組件來讀取或添加 state嫂用,添加 state 需要使用命名空間來防止與 WrappedComponent 的 state 沖突型凳。
例子:通過顯示 WrappedComponent 的 props 和 state 來 debug
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>
)
}
}
}
命名
當(dāng)通過高階組件來包裝一個組件時,你會丟失原先 WrappedComponent 的名字嘱函,可能會給開發(fā)和 debug 造成影響甘畅。
常見的解決方法是在原先的 WrappedComponent 的名字前面添加一個前綴。下面這個方法是從 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 庫已經(jīng)提供了。
案例學(xué)習(xí)
React-Redux
React-Redux 是 Redux 官方的對于 React 的綁定函似。 其中一個方法 connect 處理了所有關(guān)于監(jiān)聽 store 的 bootstrap 代碼 以及清理工作槐脏,這是通過 Props Proxy 來實現(xiàn)的。
如果你曾經(jīng)使用過 Flux 你會知道 React 組件需要和一個或多個 store 連接撇寞,并且添加/刪除對 store 的監(jiān)聽顿天,從中選擇需要的那部分 state。而 React-Redux 幫你把它們實現(xiàn)了蔑担,自己就不用再去寫這些了牌废。
Radium
Radium 是一個增強了行內(nèi)(inline)css 能力的庫,它允許了在 inline css 使用 CSS 偽選擇器钟沛。點擊此鏈接了解關(guān)于使用 inline css 的好處畔规,這是 Vjeux 做的一個演講分享,又叫做 CSS in JS恨统。
那么,Radium 是怎么允許 inline css 來實現(xiàn) CSS 偽選擇器的呢(比如 hover)三妈?它實現(xiàn)了一個反向繼承來使用渲染劫持畜埋,添加適當(dāng)?shù)氖录O(jiān)聽來模擬 CSS 偽選擇器。這要求 Radium 讀取整個 WrappedComponent 將要渲染的元素樹畴蒲,每當(dāng)找個某個元素帶有 style prop悠鞍,它就添加對應(yīng)的時間監(jiān)聽 props。簡單地說模燥,Radium 修改了原先元素樹的 props(實際上會更復(fù)雜咖祭,但這么說你可以理解到要點所在)。
Radium 只暴露了一個非常簡單的 API 給開發(fā)者蔫骂。這非常驚艷么翰,因為開發(fā)者幾乎不會注意到它的存在和它是怎么發(fā)揮作用的,而實現(xiàn)了想要的功能辽旋。這揭露了高階組件的能力浩嫌。
附錄 A:高階組件和參數(shù)
以下內(nèi)容不是必須閱讀的檐迟,你可以略過。
有時码耐,在高階組件中使用參數(shù)是很有用的追迟。這個在以上所有例子中都不是很明顯,但是對于中等的 JavaScript 開發(fā)者是比較自然的事情骚腥。讓我們迅速的介紹一下敦间。
例子:一個簡單的 Props Proxy 高階組件搭配參數(shù)。重點是這個 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:和父組件的不同之處
以下內(nèi)容不是必須閱讀的廓块,你可以略過。
父組件就是單純的 React 組件包含了一些子組件(children)纯露。React 提供了獲取和操作一個組件的 children 的 APIs剿骨。
例子:父組件獲取它的 children
class Parent extends React.Component {
render() {
return (
<div>
{this.props.children}
</div>
)
}
}
render((
<Parent>
{children}
</Parent>
), mountNode)
現(xiàn)在來總結(jié)一下父組件能做和不能做的事情(與高階組件對比):
- 渲染劫持
- 操作內(nèi)部 props
- 抽象 state。但是有缺點埠褪,不能再父組件外獲取到它的 state浓利,除非明確地實現(xiàn)了鉤子。
- 與新的 React Element 包裝钞速。這似乎是唯一一點贷掖,使用父組件要比高階組件強,但高階組件也同樣可以實現(xiàn)渴语。
- Children 的操控苹威。如果 children 不是單一 root,則需要多添加一層來包括所有 children驾凶,可能會使你的 markup 變得有點笨重牙甫。使用高階組件可以保證單一 root。
- 父組件可以在元素樹立隨意使用调违,它們不像高階組件一樣限制于一個組件窟哺。
通常來講,能使用父組件達(dá)到的效果技肩,盡量不要用高階組件且轨,因為高階組件是一種更 hack 的方法,但同時也有更高的靈活性虚婿。
總結(jié)
希望你在讀完這篇文章后旋奢,能對 React 高階組件多一絲了解。它們在多個庫中被證明非常有效然痊。
React 帶來了很多創(chuàng)新至朗,人們維護著像 Radium,React-Redux玷过,React-Router 之類的項目爽丹,都是很好的證明筑煮。
如果你想聯(lián)系我,請在 Twitter 關(guān)注我并 @franleplant粤蝎。
這個倉庫里有我為了寫這篇文章而寫的一些代碼真仲,有興趣可以看看。