如何解决 Render Props 的回调地狱

做者: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 回调地狱。

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

愿文:dmitripavlutin.com/solve-react…


交流

干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。

github.com/qq449245884…

我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,便可看到福利,你懂的。

clipboard.png
相关文章
相关标签/搜索