做者:Dmitri Pavlutinhtml
譯者:前端小智前端
來源:Dmitri Pavlutinreact
點贊再看,養成習慣git
本文
GitHub
github.com/qq449245884… 上已經收錄,更多往期高贊文章的分類,也整理了不少個人文檔,和教程資料。歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。github
術語 「render prop」 是指一種在 React 組件之間使用一個值爲函數的 prop 共享代碼的簡單技術面試
簡而言之,只要一個組件中某個屬性的值是函數,那麼就能夠說該組件使用了 Render Props 這種技術。聽起來好像就那麼回事兒,那到底 Render Props 有哪些應用場景呢,我們仍是從簡單的例子講起,假如我們要實現一個展現我的信息的組件,一開始可能會這麼實現:數組
const PersonInfo = props => (
<div>
<h1>姓名:{props.name}</h1>
</div>
);
// 調用
<PersonInfo name='前端小智'/>
複製代碼
若是,想要在 PersonInfo 組件上還須要一個年齡呢,我們會這麼實現:異步
const PersonInfo = props => (
<div>
<h1>姓名:{props.name}</h1>
<p>年齡:{props.age}</[>
</div>
);
// 調用
<PersonInfo name='前端小智' age='18'/>
複製代碼
而後若是還要加上連接呢,又要在 PersonInfo
組件的內部實現發送連接的邏輯,很明顯這種方式違背了軟件開發六大原則之一的 開閉原則,即每次修改都要到組件內部需修改。函數
開閉原則:對修改關閉,對拓展開放。工具
那有什麼方法能夠避免這種方式的修改呢?
在原生 JS 中,若是我們調用函數後,還要作些騷操做,我們通常使用回調函數來處理這種狀況。
在 React 中我們可使用 Render Props
,其實和回調同樣:
const PersonInfo = props => { return props.render(props); }
// 使用
<PersonInfo
name='前端小智' age = '18' link = 'link'
render = {(props) => {
<div>
<h1>{props.name}</h1>
<p>{props.age}</p>
<a href="props.link"></a>
</div>
}}
/>
複製代碼
值得一提的是,並非只有在 render
屬性中傳入函數才能叫 Render Props
,實際上任何屬性只要它的值是函數,均可稱之爲 Render Props
,好比上面這個例子把 render
屬性名改爲 children
的話使用上其實更爲簡便:
const PersonInfo = props => {
return props.children(props);
};
<PersonInfo name='前端小智' 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 回調地獄。
假設各位須要檢測並顯示網站訪問者所在的城市。
首先,須要肯定用戶地理座標的組件,像<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
回調地獄的問題。
我們換中更好的組件設計,以排除回調的嵌套問題。
爲了將回調的嵌套轉換爲可讀性更好的代碼,我們將回調重構爲類的方法。
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()
中。這樣的組件設計更容易理解,由於渲染邏輯封裝在一個單獨的方法中。
若是須要更多嵌套,類的方式是垂直增長(經過添加新方法),而不是水平(經過相互嵌套函數),回調地獄問題消失。
方法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
的值 。
若是我們想要一個不涉及建立類的更輕鬆的方法,能夠簡單地使用函數組合。
使用函數組合重構 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()
。
若是須要訪問渲染函數中的 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()
的新函數實例。
前面提到的類方法可能更適合使用。同時,這些方法不會在每次從新渲染時從新建立。
若是想要在如何處理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
的結果,那麼類組件和函數組合方法並不合適。
想象一下,當我們渲染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
可能會更好:
import { 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, result2, result3 }) => (
<span>
Fetch result 1: {result1}
Fetch result 2: {result2}
Fetch result 3: {result3}
</span>
)}
</Composed>
);
}
// Somewhere
<MultipleFetchResult />
複製代碼
在函數({result1, result2, result3}) =>{…}
提供給<Composed>
。所以,我們沒必要手動綁定參數或嵌套回調。
固然,react-adopt
的代價是要學習額外的抽象,並略微增長應用程序的大小。
Render prop是一種設計 React 組件的有效技術。然而,影響其可用性的一個問題是回調地獄
。函數組合或類組件方法能夠解決回調地獄
的問題。
可是,若是有一個更復雜的狀況,使用多個 Render prop 回調函數使用彼此的結果,那麼react-adopt
是一個很好的解決方法。
你知道其餘有效的方法來解決Render prop 回調地獄嗎? 歡迎留言討論。
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
願文:dmitripavlutin.com/solve-react…
乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。
我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!
關注公衆號,後臺回覆福利,便可看到福利,你懂的。