?
本文檔使用 php中文網(wǎng)手冊(cè) 發(fā)布
高階組件(HOC)是React中用于重用組件邏輯的高級(jí)技術(shù)。HOC本身不是React API的一部分。它們是從React的構(gòu)圖本質(zhì)中浮現(xiàn)出來(lái)的一種模式。
具體而言,高階組件是一個(gè)接收組件并返回新組件的函數(shù)。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
盡管組件將道具轉(zhuǎn)換為UI,但高階組件會(huì)將組件轉(zhuǎn)換為另一個(gè)組件。
HOC在第三方React庫(kù)中很常見(jiàn),例如Redux connect
和Relay createContainer
。
在本文中,我們將討論為什么高階組件有用,以及如何編寫自己的。
注意 我們以前推薦mixin作為處理交叉問(wèn)題的一種方式。之后我們意識(shí)到mixin會(huì)造成比他們的價(jià)值更大的麻煩。了解更多關(guān)于我們?yōu)槭裁措x開(kāi)mixin以及如何轉(zhuǎn)換現(xiàn)有組件的更多信息。
組件是React中代碼重用的主要單元。但是,您會(huì)發(fā)現(xiàn)某些模式不適合傳統(tǒng)組件。
例如,假設(shè)您有一個(gè)CommentList
組件訂閱外部數(shù)據(jù)源來(lái)呈現(xiàn)評(píng)論列表:
class CommentList extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = { // "DataSource" is some global data source comments: DataSource.getComments() }; } componentDidMount() { // Subscribe to changes DataSource.addChangeListener(this.handleChange); } componentWillUnmount() { // Clean up listener DataSource.removeChangeListener(this.handleChange); } handleChange() { // Update component state whenever the data source changes this.setState({ comments: DataSource.getComments() }); } render() { return ( <div> {this.state.comments.map((comment) => ( <Comment comment={comment} key={comment.id} /> ))} </div> ); }}
之后,您將編寫一個(gè)組件訂閱單個(gè)博客帖子,該帖子遵循類似的模式:
class BlogPost extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = { blogPost: DataSource.getBlogPost(props.id) }; } componentDidMount() { DataSource.addChangeListener(this.handleChange); } componentWillUnmount() { DataSource.removeChangeListener(this.handleChange); } handleChange() { this.setState({ blogPost: DataSource.getBlogPost(this.props.id) }); } render() { return <TextBlock text={this.state.blogPost} />; }}
CommentList
并且BlogPost
不完全相同 - 它們調(diào)用不同的方法DataSource
,并且它們呈現(xiàn)不同的輸出。但是他們的大部分實(shí)現(xiàn)都是一樣的:
在mount上,添加一個(gè)更改監(jiān)聽(tīng)器DataSource
。
在監(jiān)聽(tīng)器內(nèi)部,setState
每當(dāng)數(shù)據(jù)源發(fā)生變化時(shí)都會(huì)調(diào)用。
在卸載時(shí),刪除更改偵聽(tīng)器。
你可以想象,在一個(gè)大型應(yīng)用程序中,訂閱DataSource
和調(diào)用的相同模式setState
將會(huì)一遍又一遍地發(fā)生。我們需要一種抽象,使我們能夠在單個(gè)地方定義這種邏輯,并在多個(gè)組件之間共享這些邏輯。這是高階元件擅長(zhǎng)的地方。
我們可以編寫一個(gè)創(chuàng)建組件的函數(shù),比如CommentList
和BlogPost
訂閱DataSource
。該函數(shù)將接受作為其參數(shù)之一的接收訂閱數(shù)據(jù)作為道具的子組件。我們來(lái)調(diào)用這個(gè)函數(shù)withSubscription
:
const CommentListWithSubscription = withSubscription( CommentList, (DataSource) => DataSource.getComments());const BlogPostWithSubscription = withSubscription( BlogPost, (DataSource, props) => DataSource.getBlogPost(props.id));
第一個(gè)參數(shù)是包裝組件。第二個(gè)參數(shù)檢索我們感興趣的數(shù)據(jù),給出一個(gè)DataSource
和當(dāng)前的道具。
當(dāng)CommentListWithSubscription
與BlogPostWithSubscription
被渲染,CommentList
并且BlogPost
將傳遞一個(gè)data
與從檢索到的最新的數(shù)據(jù)道具DataSource
:
// This function takes a component... function withSubscription(WrappedComponent, selectData) { // ...and returns another component... return class extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = { data: selectData(DataSource, props) }; } componentDidMount() { // ... that takes care of the subscription... DataSource.addChangeListener(this.handleChange); } componentWillUnmount() { DataSource.removeChangeListener(this.handleChange); } handleChange() { this.setState({ data: selectData(DataSource, this.props) }); } render() { // ... and renders the wrapped component with the fresh data! // Notice that we pass through any additional props return <WrappedComponent data={this.state.data} {...this.props} />; } };}
請(qǐng)注意,HOC不會(huì)修改輸入組件,也不會(huì)使用繼承來(lái)復(fù)制其行為。相反,HOC 通過(guò)將其包裝在容器組件中來(lái)組成原始組件。HOC是一種純粹的功能,具有零副作用。
就是這樣!被包裝的組件接收容器的所有道具以及一個(gè)新的道具,data
它用來(lái)渲染其輸出。HOC不關(guān)心如何或?yàn)槭裁词褂脭?shù)據(jù),并且封裝的組件不關(guān)心數(shù)據(jù)來(lái)自何處。
因?yàn)?code>withSubscription是一個(gè)正常的函數(shù),所以你可以添加盡可能多或者很少的參數(shù)。例如,您可能希望使data
prop 的名稱可配置,以進(jìn)一步將HOC與封裝組件隔離?;蛘吣梢越邮芘渲玫膮?shù)shouldComponentUpdate
,或者配置數(shù)據(jù)源的參數(shù)。這些都是可能的,因?yàn)镠OC完全控制組件的定義。
與組件一樣,合約withSubscription
與包裝組件之間的合約完全基于道具。這可以很容易地將一個(gè)HOC換成另一個(gè)HOC,只要它們?yōu)榘b組件提供相同的道具。例如,如果您更改數(shù)據(jù)提取庫(kù),這可能很有用。
抵制HOC內(nèi)部修改組件原型(或者改變它)的誘惑。
function logProps(InputComponent) { InputComponent.prototype.componentWillReceiveProps = function(nextProps) { console.log('Current props: ', this.props); console.log('Next props: ', nextProps); }; // The fact that we're returning the original input is a hint that it has // been mutated. return InputComponent;} // EnhancedComponent will log whenever props are receivedconst EnhancedComponent = logProps(InputComponent);
這有幾個(gè)問(wèn)題。一個(gè)是輸入組件不能與增強(qiáng)組件分開(kāi)重復(fù)使用。更關(guān)鍵的是,如果你申請(qǐng)的另一個(gè)HOC到EnhancedComponent
那個(gè)也發(fā)生變異componentWillReceiveProps
,第一HOC的功能將被改寫!這個(gè)HOC也不能用于沒(méi)有生命周期方法的函數(shù)組件。
突變HOC是一個(gè)漏洞抽象 - 消費(fèi)者必須知道它們是如何實(shí)施的,以避免與其他HOC發(fā)生沖突。
通過(guò)將輸入組件包裝在容器組件中,HOC不應(yīng)該使用變異,而應(yīng)該使用組合:
function logProps(WrappedComponent) { return class extends React.Component { componentWillReceiveProps(nextProps) { console.log('Current props: ', this.props); console.log('Next props: ', nextProps); } render() { // Wraps the input component in a container, without mutating it. Good! return <WrappedComponent {...this.props} />; } }}
這個(gè)HOC具有與變種版本相同的功能,同時(shí)避免了沖突的可能性。它與類和功能組件一樣有效。而且因?yàn)樗且粋€(gè)純粹的功能,它可以與其他HOC組合,甚至可以與其自身組合。
您可能已經(jīng)注意到HOC和稱為容器組件的模式之間的相似之處。集裝箱組件是在高層和低層關(guān)注點(diǎn)之間分離責(zé)任戰(zhàn)略的一部分。容器管理諸如訂閱和狀態(tài)之類的東西,并將道具傳遞給處理諸如呈現(xiàn)UI之類的事物的組件。HOC使用容器作為其實(shí)施的一部分。您可以將HOC視為參數(shù)化容器組件定義。
HOC向組件添加功能。他們不應(yīng)該大幅改變合同。預(yù)計(jì)從HOC返回的組件具有與被包裝組件類似的接口。
HOC應(yīng)該通過(guò)與其特定關(guān)注無(wú)關(guān)的道具。大多數(shù)HOC包含一個(gè)類似于下面的渲染方法:
render() { // Filter out extra props that are specific to this HOC and shouldn't be // passed through const { extraProp, ...passThroughProps } = this.props; // Inject props into the wrapped component. These are usually state values or // instance methods. const injectedProp = someStateOrInstanceMethod; // Pass props to wrapped component return ( <WrappedComponent injectedProp={injectedProp} {...passThroughProps} /> ); }
此慣例有助于確保HOC盡可能靈活且可重用。
并非所有HOC看起來(lái)都一樣。有時(shí)他們只接受一個(gè)參數(shù),包裝組件:
const NavbarWithRouter = withRouter(Navbar);
HOC通常會(huì)接受其他參數(shù)。在Relay的這個(gè)例子中,一個(gè)配置對(duì)象被用來(lái)指定一個(gè)組件的數(shù)據(jù)依賴關(guān)系:
const CommentWithRelay = Relay.createContainer(Comment, config);
HOC最常見(jiàn)的簽名如下所示:
// React Redux's `connect`const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
什么?!如果你把它分開(kāi),很容易看到發(fā)生了什么。
// connect is a function that returns another functionconst enhance = connect(commentListSelector, commentListActions); // The returned function is an HOC, which returns a component that is connected // to the Redux storeconst ConnectedComment = enhance(CommentList);
換句話說(shuō),connect
是一個(gè)返回高階組件的高階函數(shù)!
這種形式可能看起來(lái)很混亂或不必要,但它有一個(gè)有用的特性。單參數(shù)HOC(如connect
函數(shù)返回的HOC)具有簽名Component => Component
。輸出類型與輸入類型相同的函數(shù)非常容易組合在一起。
// Instead of doing this...const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent)) // ... you can use a function composition utility // compose(f, g, h) is the same as (...args) => f(g(h(...args)))const enhance = compose( // These are both single-argument HOCs withRouter, connect(commentSelector))const EnhancedComponent = enhance(WrappedComponent)
(這個(gè)屬性也允許使用connect
其他增強(qiáng)器樣式的HOC作為裝飾器,這是一個(gè)實(shí)驗(yàn)性JavaScript提案。)
所述compose
效用函數(shù)是由許多第三方庫(kù)包括lodash(如提供lodash.flowRight
),終極版,和Ramda。
由HOC創(chuàng)建的容器組件像任何其他組件一樣出現(xiàn)在React Developer Tools中。為了便于調(diào)試,選擇一個(gè)顯示名稱來(lái)傳達(dá)它是HOC的結(jié)果。
最常用的技術(shù)是封裝包裝組件的顯示名稱。因此,如果您的高階組件被命名withSubscription
,并且包裝組件的顯示名稱是CommentList
,則使用顯示名稱WithSubscription(CommentList)
:
function withSubscription(WrappedComponent) { class WithSubscription extends React.Component {/* ... */} WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`; return WithSubscription;}function getDisplayName(WrappedComponent) { return WrappedComponent.displayName || WrappedComponent.name || 'Component'; }
如果您是React的新手,那么高階組件會(huì)附帶一些注意事項(xiàng),這些注意事項(xiàng)不會(huì)立即顯現(xiàn)出來(lái)。
React的差異算法(稱為reconciliation)使用組件標(biāo)識(shí)來(lái)確定它是應(yīng)該更新現(xiàn)有的子樹(shù)還是將其丟棄并掛載新的子樹(shù)。如果返回的組件與來(lái)自先前渲染的組件render
相同(===
),則React通過(guò)用新組件區(qū)分它來(lái)遞歸更新子樹(shù)。如果不相等,則前一個(gè)子樹(shù)完全卸載。
通常情況下,你不需要考慮這一點(diǎn)。但是它對(duì)于HOC很重要,因?yàn)樗馕吨銦o(wú)法將HOC應(yīng)用到組件的渲染方法中的組件:
render() { // A new version of EnhancedComponent is created on every render // EnhancedComponent1 !== EnhancedComponent2 const EnhancedComponent = enhance(MyComponent); // That causes the entire subtree to unmount/remount each time! return <EnhancedComponent />; }
這里的問(wèn)題不僅僅是性能 - 重新安裝組件會(huì)導(dǎo)致組件及其所有子組件的狀態(tài)丟失。
相反,在組件定義之外應(yīng)用HOC,以便只生成一次結(jié)果組件。那么,它的身份將在整個(gè)渲染過(guò)程中保持一致。無(wú)論如何,這通常是你想要的。
在您需要?jiǎng)討B(tài)應(yīng)用HOC的罕見(jiàn)情況下,您也可以在組件的生命周期方法或其構(gòu)造函數(shù)中執(zhí)行此操作。
有時(shí)在React組件上定義靜態(tài)方法很有用。例如,中繼容器公開(kāi)了一個(gè)靜態(tài)方法getFragment
來(lái)促進(jìn)GraphQL片段的組合。
但是,如果將HOC應(yīng)用于組件,則原始組件將使用容器組件進(jìn)行包裝。這意味著新組件沒(méi)有任何原始組件的靜態(tài)方法。
// Define a static methodWrappedComponent.staticMethod = function() {/*...*/} // Now apply an HOCconst EnhancedComponent = enhance(WrappedComponent); // The enhanced component has no static methodtypeof EnhancedComponent.staticMethod === 'undefined' // true
為了解決這個(gè)問(wèn)題,你可以在返回之前將這些方法復(fù)制到容器中:
function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} // Must know exactly which method(s) to copy :( Enhance.staticMethod = WrappedComponent.staticMethod; return Enhance;}
但是,這需要您確切地知道需要復(fù)制哪些方法。您可以使用hoist-non-react-statics來(lái)自動(dòng)復(fù)制所有非React靜態(tài)方法:
import hoistNonReactStatic from 'hoist-non-react-statics';function enhance(WrappedComponent) { class Enhance extends React.Component {/*...*/} hoistNonReactStatic(Enhance, WrappedComponent); return Enhance;}
另一種可能的解決方案是將靜態(tài)方法與組件本身分開(kāi)導(dǎo)出。
// Instead of...MyComponent.someFunction = someFunction;export default MyComponent; // ...export the method separately...export { someFunction }; // ...and in the consuming module, import bothimport MyComponent, { someFunction } from './MyComponent.js';
雖然高階組件的慣例是將所有道具傳遞給包裝組件,但不可能通過(guò)參考。這是因?yàn)?code>ref它不是一個(gè)真正的道具key
,它是由React專門處理的。如果將ref添加到其組件是HOC結(jié)果的元素,則ref引用最外層容器組件的實(shí)例,而不是包裝組件。
如果你發(fā)現(xiàn)自己面臨這個(gè)問(wèn)題,理想的解決方案是找出如何避免使用ref
。偶爾,剛剛接觸React范例的用戶依賴于在支撐物更好地工作的情況下的參考。
也就是說(shuō),有些時(shí)候refs是必要的逃生艙口,否則React不會(huì)支持它們。聚焦輸入字段是一個(gè)例子,您可能需要對(duì)組件進(jìn)行必要的控制。在這種情況下,一種解決方案是通過(guò)給它一個(gè)不同的名稱來(lái)傳遞一個(gè)ref回調(diào)作為普通道具:
function Field({ inputRef, ...rest }) { return <input ref={inputRef} {...rest} />;} // Wrap Field in a higher-order componentconst EnhancedField = enhance(Field); // Inside a class component's render method...<EnhancedField inputRef={(inputEl) => { // This callback gets passed through as a regular prop this.inputEl = inputEl }}/>// Now you can call imperative methodsthis.inputEl.focus();
這不是一個(gè)完美的解決方案。我們更喜歡參考資料仍然是圖書館關(guān)注的問(wèn)題,而不是要求您手動(dòng)處理它們。我們正在探索解決這個(gè)問(wèn)題的方法,以便使用HOC是不可觀測(cè)的。