如何解決 Render Props 的回調地獄

做者:Dmitri Pavlutin
譯者:前端小智
來源:Dmitri Pavlutin
點贊再看,養成習慣

本文 GitHub https://github.com/qq44924588... 上已經收錄,更多往期高贊文章的分類,也整理了不少個人文檔,和教程資料。歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。html


術語 「render prop」 是指一種在 React 組件之間使用一個值爲函數的 prop 共享代碼的簡單技術

簡而言之,只要一個組件中某個屬性的值是函數,那麼就能夠說該組件使用了 Render Props 這種技術。聽起來好像就那麼回事兒,那到底 Render Props 有哪些應用場景呢,我們仍是從簡單的例子講起,假如我們要實現一個展現我的信息的組件,一開始可能會這麼實現:前端

const PersonInfo = props => (
  <div>
    <h1>姓名:{props.name}</h1>
  </div>
);

// 調用
<PersonInfo name='前端小智'/>

若是,想要在 PersonInfo 組件上還須要一個年齡呢,我們會這麼實現:react

const PersonInfo = props => (
  <div>
    <h1>姓名:{props.name}</h1>
    <p>年齡:{props.age}</[>
  </div>
);

// 調用
<PersonInfo name='前端小智' age='18'/>

而後若是還要加上連接呢,又要在 PersonInfo 組件的內部實現發送連接的邏輯,很明顯這種方式違背了軟件開發六大原則之一的 開閉原則,即每次修改都要到組件內部需修改。git

開閉原則:對修改關閉,對拓展開放。

那有什麼方法能夠避免這種方式的修改呢?github

在原生 JS 中,若是我們調用函數後,還要作些騷操做,我們通常使用回調函數來處理這種狀況。面試

在 React 中我們可使用 Render Props,其實和回調同樣:segmentfault

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 回調地獄。

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回調的結果時(AsyncFetch1AsyncFetch2AsyncFetch3)

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

願文:https://dmitripavlutin.com/so...


交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq44924588...

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

clipboard.png

相關文章
相關標籤/搜索