通過例項瞭解Render Props回撥地獄解決方案
簡而言之,只要一個元件中某個屬性的值是函式,那麼就可以說該元件使用了 Render Props 這種技術。聽起來好像就那麼回事兒,那到底 Render Props 有哪些應用場景呢,咱們還是從簡單的例子講起,假如咱們要實現一個展示個人資訊的元件,一開始可能會這麼實現:
const PersonInfo = props => ( <div> <h1>姓名:{props.name}</h1> </div> ); // 呼叫 <PersonInfo name='web前端'/>
如果,想要在 PersonInfo 元件上還需要一個年齡呢,咱們會這麼實現:
const PersonInfo = props => ( <div> <h1>姓名:{props.name}</h1> <p>年齡:{props.age}</[> </div> ); // 呼叫 <PersonInfo name='web前端' age='18'/>
然後如果還要加上鍊接呢,又要在 PersonInfo 元件的內部實現傳送連結的邏輯,很明顯這種方式違背了軟體開發六大原則之一的 開閉原則,即每次修改都要到元件內部需修改。
開閉原則:對修改關閉,對拓展開放。
那有什麼方法可以避免這種方式的修改呢?
在原生 js 中,如果咱們呼叫函式後,還要做些騷操作,咱們一般使用回撥函式來處理這種情況。
在 react 中咱們可以使用 Render Props,其實和回撥一樣:
const PersonInfo = props => { return props.render(props); } // 使用 <PersonInfo name='web前端' age = '18' link = 'link' render = {(props) => { <div> <h1>{props.name}</h1> <p>{props.age}</p> <a href="props.link" rel="external nofollow" ></a> </div> }} />
值得一提的是,並不是只有在 render 屬性中傳入函式才能叫 Render Props,實際上任何屬性只要它的值是函式,都可稱之為 Render Props,比如上面這個例子把 render 屬性名改成 children 的話使用上其實更為簡便:
const PersonInfo = props => { return props.children(props); }; <PersonInfo name='web前端' age = '18' link = 'link'> {(props) => ( <div> <h1>{props.name}</h1> <p>{props.age}</p> <a href={props.link}></a> </div> )} </PersonInfo
這樣就可以直接在 PersonInfo 標籤內寫函數了,比起之前在 render 中更為直觀。
所以,React 中的 Render Props 你可以把它理解成 js 中的回撥函式。
React 元件的良好設計是可維護且易於更改程式碼的關鍵。
從這個意義上說,React 提供了許多設計技術,比如組合、Hooks、高階元件、Render Props等等。
Render props 可以有效地以鬆散耦合的方式設計元件。它的本質在於使用一個特殊的prop(通常稱為render),將渲染邏輯委託給父元件。
import Mouse from 'Mouse'; function ShowMousePosition() { return ( <Mouse render = { ({ x,y }) => <div>Position: {x}px,{y}px</div> } /> ) }
使用此模式時,遲早會遇到在多個 render prop 回撥中巢狀元件的問題: render props 回撥地獄。
1. Render Props 的回撥地獄
假設各位需要檢測並顯示網站訪問者所在的城市。
首先,需要確定使用者地理座標的元件,像<AsyncCoords render={coords => ... } 這樣的元件進行非同步操作,使用 Geolocation API,然後呼叫Render prop 進行回撥。。
然後用獲取的座標用來近似確定使用者的城市:<AsyncCity lat={lat} long={long} render={city => ...} />,這個元件也叫Render prop。
接著咱們將這些非同步元件合併到<DetectCity>元件中
function DetectCity() { return ( <AsyncCoords render={({ lat,long }) => { return ( <AsyncCity lat={lat} long={long} render={city => { if (city == null) { return <div>Unable to detect city.</div>; } return <div>You might be in {city}.</div>; }} /> ); }} /> ); } // 在某處使用 <DetectCity />
可能已經發現了這個問題:Render Prop回撥函式的巢狀。巢狀的回撥函式越多,程式碼就越難理解。這是Render Prop回撥地獄的問題。
咱們換中更好的元件設計,以排除回撥的巢狀問題。
2. Class 方法
為了將回調的巢狀轉換為可讀性更好的程式碼,咱們將回調重構為類的方法。
class DetectCity extends React.Component { render() { return <AsyncCoords render={this.renderCoords} />; } renderCoords = ({ lat,long }) => { return <AsyncCity lat={lat} long={long} render={this.renderCity}/>; } renderCity = city => { if (city == null) { return <div>Unable to detect city.</div>; } return <div>You might be in {city}.</div>; } } // 在某處使用 <DetectCity />
回撥被提取到分開的方法renderCoords()和renderCity()中。這樣的元件設計更容易理解,因為渲染邏輯封裝在一個單獨的方法中。
如果需要更多巢狀,類的方式是垂直增加(通過新增新方法),而不是水平(通過相互巢狀函式),回撥地獄問題消失。
2.1 訪問渲染方法內部的元件 props
方法renderCoors()和renderCity()是使用箭頭函法定義的,這樣可以將 this 繫結到元件例項,所以可以在<AsyncCoords>和<AsyncCity>元件中呼叫這些方法。
有了this作為元件例項,就可以通過 prop 獲取所需要的內容:
class DetectCityMessage extends React.Component { render() { return <AsyncCoords render={this.renderCoords} />; } renderCoords = ({ lat,long }) => { return <AsyncCity lat={lat} long={long} render={this.renderCity}/>; } renderCity = city => { // 看這 const { noCityMessage } = this.props; if (city == null) { return <div>{noCityMessage}</div>; } return <div>You might be in {city}.</div>; } } <DetectCityMessage noCityMessage="Unable to detect city." />
renderCity()中的this值指向<DetectCityMessage>元件例項。現在就很容易從this.props獲取 noCityMessage 的值 。
3. 函式組合方法
如果咱們想要一個不涉及建立類的更輕鬆的方法,可以簡單地使用函式組合。
使用函式組合重構 DetectCity 元件:
function DetectCity() { return <AsyncCoords render={renderCoords} />; } function renderCoords({ lat,long }) { return <AsyncCity lat={lat} long={long} render={renderCity}/>; } function renderCity(city) { if (city == null) { return <div>Unable to detect city.</div>; } return <div>You might be in {city}.</div>; } // Somewhere <DetectCity />
現在,常規函式renderCoors()和renderCity()封裝了渲染邏輯,而不是用方法建立類。
如果需要更多巢狀,只需要再次新增新函式即可。程式碼垂直增長(通過新增新函式),而不是水平增長(通過巢狀),從而解決回撥地獄問題。
這種方法的另一個好處是可以單獨測試渲染函式:renderCoords()和renderCity()。
3.1 訪問渲染函式內部元件的 prop
如果需要訪問渲染函式中的 prop ,可以直接將渲染函式插入元件中
function DetectCityMessage(props) { return ( <AsyncCoords render={renderCoords} /> ); function renderCoords({ lat,long }) { return ( <AsyncCity lat={lat} long={long} render={renderCity} /> ); } function renderCity(city) { const { noCityMessage } = props; if (city == null) { return <div>{noCityMessage}</div>; } return <div>You might be in {city}.</div>; } } // Somewhere <DetectCityMessage noCityMessage="Unknown city." />
雖然這種結構有效,但我不太喜歡它,因為每次<DetectCityMessage>重新渲染時,都會建立renderCoords()和renderCity()的新函式例項。
前面提到的類方法可能更適合使用。同時,這些方法不會在每次重新渲染時重新建立。
4. 實用的方法
如果想要在如何處理render props回撥方面具有更大的靈活性,那麼使用React-adopt是一個不錯的選擇。
使用 react-adopt 來重構 <DetectCity> 元件:
import { adopt } from 'react-adopt'; const Composed = adopt({ coords: ({ render }) => <AsyncCoords render={render} />,city: ({ coords: { lat,long },render }) => ( <AsyncCity lat={lat} long={long} render={render} /> ) }); function DetectCity() { return ( <Composed> { city => { if (city == null) { return <div>Unable to detect city.</div>; } return <div>You might be in {city}.</div>; }} </Composed> ); } <DetectCity />
react-adopt需要一個特殊的對映器來描述非同步操作的順序。同時,庫負責建立定製的渲染回撥,以確保正確的非同步執行順序。
你可能會注意到的,上面使用react-adopt 的示例比使用類元件或函式組合的方法需要更多的程式碼。那麼,為什麼還要使用“react-adopt”呢?
不幸的是,如果需要聚合多個render props的結果,那麼類元件和函式組合方法並不合適。
4.1 聚合多個渲染道具結果
想象一下,當咱們渲染3個render prop回撥的結果時(AsyncFetch1、AsyncFetch2、AsyncFetch3)
function MultipleFetchResult() { return ( <AsyncFetch1 render={result1 => ( <AsyncFetch2 render={result2 => ( <AsyncFetch3 render={result3 => ( <span> Fetch result 1: {result1} Fetch result 2: {result2} Fetch result 3: {result3} </span> )} /> )} /> )} /> ); } <MultipleFetchResult />
<MultipleFetchResult>元件沉浸所有3個非同步獲取操作的結果,這是一個闊怕回撥地獄的情況。
如果嘗試使用類元件或函式的組合方法,它會很麻煩。 回撥地獄轉變為引數繫結地獄:
class MultipleFetchResult extends React.Component { render() { return <AsyncFetch1 render={this.renderResult1} />; } renderResult1(result1) { return ( <AsyncFetch2 render={this.renderResult2.bind(this,result1)} /> ); } renderResult2(result1,result2) { return ( <AsyncFetch2 render={this.renderResult3.bind(this,result1,result2)} /> ); } renderResult3(result1,result2,result3) { return ( <span> Fetch result 1: {result1} Fetch result 2: {result2} Fetch result 3: {result3} </span> ); } } // Somewhere <MultipleFetchResult />
咱們必須手動繫結render prop回撥的結果,直到它們最終到達renderResult3()方法。
如果不喜歡手工繫結,那麼採用react-adopt可能會更好:
mport { adopt } from 'react-adopt'; const Composed = adopt({ result1: ({ render }) => <AsyncFetch1 render={render} />,result2: ({ render }) => <AsyncFetch2 render={render} />,result3: ({ render }) => <AsyncFetch3 render={render} /> }); function MultipleFetchResult() { return ( <Composed> {({ result1,result3 }) => ( <span> Fetch result 1: {result1} Fetch result 2: {result2} Fetch result 3: {result3} </span> )} </Composed> ); } // Somewhere <MultipleFetchResult />
在函式({result1,result3}) =>{…}提供給<Composed>。因此,咱們不必手動繫結引數或巢狀回撥。
當然,react-adopt的代價是要學習額外的抽象,並略微增加應用程式的大小。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。