通过实例了解Render Props回调地狱解决方案


Posted in Javascript onNovember 04, 2020

简而言之,只要一个组件中某个属性的值是函数,那么就可以说该组件使用了 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, 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的代价是要学习额外的抽象,并略微增加应用程序的大小。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js文字滚动停顿效果代码
Jun 28 Javascript
深入理解JavaScript中的传值与传引用
Dec 09 Javascript
jQuery内容折叠效果插件用法实例分析(附demo源码)
Apr 28 Javascript
Bootstrap框架动态生成Web页面文章内目录的方法
May 12 Javascript
JavaScript中判断数据类型的方法总结
May 24 Javascript
jQuery解决input元素的blur事件和其他非表单元素的click事件冲突问题
Aug 15 Javascript
JQuery实现列表中复选框全选反选功能封装(推荐)
Nov 24 Javascript
setTimeout函数的神奇使用
Feb 26 Javascript
js实现页面导航层级指示效果
Aug 25 Javascript
Vue获取微博授权URL代码实例
Nov 04 Javascript
vue3中轻松实现switch功能组件的全过程
Jan 07 Vue.js
vue ref如何获取子组件属性值
Mar 31 Vue.js
vant中的toast层级改变操作
Nov 04 #Javascript
vant中的toast轻提示实现代码
Nov 04 #Javascript
JavaScript快速调试的两个技巧
Nov 04 #Javascript
如何实现小程序与小程序之间的跳转
Nov 04 #Javascript
Vant 中的Toast设置全局的延迟时间操作
Nov 04 #Javascript
vue 使用vant插件做tabs切换和无限加载功能的实现
Nov 04 #Javascript
Vue获取微博授权URL代码实例
Nov 04 #Javascript
You might like
在PHP的图形函数中显示汉字
2006/10/09 PHP
php 日期时间处理函数小结
2009/12/18 PHP
对比分析php中Cookie与Session的异同
2016/02/19 PHP
360搜索引擎自动收录php改写方案
2018/04/28 PHP
PHP实现提高SESSION响应速度的几种方法详解
2019/08/09 PHP
JavaScript高级程序设计 DOM学习笔记
2011/09/10 Javascript
js弹出层之1:JQuery.Boxy (二)
2011/10/06 Javascript
jQuery jcrop插件截图使用方法
2013/11/20 Javascript
JavaScript实现班级随机点名小应用需求的具体分析
2014/05/12 Javascript
jQuery中:only-child选择器用法实例
2015/01/03 Javascript
jQuery获取样式中颜色值的方法
2015/01/29 Javascript
javascript实现很浪漫的气泡冒出特效
2020/09/05 Javascript
AngularJS基础 ng-keypress 指令简单示例
2016/08/02 Javascript
微信小程序开发之录音机 音频播放 动画实例 (真机可用)
2016/12/08 Javascript
初探nodeJS
2017/01/24 NodeJs
Bootstrap组合上、下拉框简单实现代码
2017/03/06 Javascript
get  post jsonp三种数据交互形式实例详解
2017/08/25 Javascript
vue.js或js实现中文A-Z排序的方法
2018/03/08 Javascript
微信小程序数据统计和错误统计的实现方法
2019/06/26 Javascript
Node.js在图片模板上生成二维码图片并附带底部文字说明实现详解
2019/08/07 Javascript
解决layui追加或者动态修改的表单元素“没效果”的问题
2019/09/18 Javascript
Python实现利用最大公约数求三个正整数的最小公倍数示例
2017/09/30 Python
python基于ID3思想的决策树
2018/01/03 Python
解决python matplotlib imshow无法显示的问题
2018/05/24 Python
python+opencv实现霍夫变换检测直线
2020/10/23 Python
在python中使用requests 模拟浏览器发送请求数据的方法
2018/12/26 Python
python 解决selenium 中的 .clear()方法失效问题
2020/09/01 Python
html5记忆翻牌游戏实现思路及代码
2013/07/25 HTML / CSS
全球工业:Global Industrial
2020/02/01 全球购物
中介业务员岗位职责
2014/04/09 职场文书
抗洪救灾先进集体事迹材料
2014/05/26 职场文书
初中作文评语集锦
2014/12/25 职场文书
《云雀的心愿》教学反思
2016/02/23 职场文书
读《工匠精神》有感:热爱工作,精益求精
2019/12/28 职场文书
关于EntityWrapper的in用法
2022/03/22 Java/Android
阿里云ECS云服务器快照的概念以及如何使用
2022/04/21 Servers