通过实例了解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, 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的代价是要学习额外的抽象,并略微增加应用程序的大小。

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

(0)

相关推荐

  • Vue Render函数创建DOM节点代码实例

    虽然在render里使用createElement函数创建DOM节点不是很直观,但是在部分独立组件的设计中还是可以满足一些特殊需求的.一个简单的render示例如下: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device

  • Vue自定义render统一项目组弹框功能

    一.本文收获 pick 二.为什么要统一封装弹框: 要封装成怎样 通过举例常规弹框的写法.我们可以体会到,通常要弹出一个页面,需要创建一个页面 normalDialog.vue 包裹 dialogBody.vue (弹框主体):需要 parent.vue 设置flag控制弹框显示隐藏, normalDialog.vue 关闭的时候设置 parent.vue 对应 flag .缺点: 流程繁杂.配置繁琐.不灵活.样式不统一和参数传递麻烦等 .如果一个项目弹框较多的时候,弊端将会更明显,大量的 is

  • 使用react render props实现倒计时的示例代码

    react的组件模式可以观看Michael Chan的演讲视频,平时大家常听到的react模式也是HOC, HOC的使用场景很多,譬如react-redux的connect,这里不赘述HOC相关,感兴趣可以自行了解. 首先是这样一个场景,我的业务需要实现倒计时,倒计时你懂得,倒计时经常应用在预告一个活动的开始,像秒杀,像开售抢购等,或者活动的截止. 我们来梳理一下这个倒计时的功能: 定时更新时间,以秒为度; 可以更新倒计时的截止时间,比如从10月1日更新为10月2日: 倒计时结束,执行对应结束逻

  • 何时/使用 Vue3 render 函数的教程详解

    什么是 DOM? 如果我们把这个 HTML 加载到浏览器中,浏览器创建这些节点,用来显示网页.所以这个HTML映射到一系列DOM节点,然后我们可以使用JavaScript进行操作.例如: let item = document.getElementByTagName('h1')[0] item.textContent = "New Heading" VDOM 网页可以有很多DOM节点,这意味着DOM树可以有数千个节点.这就是为什么我们有像Vue这样的框架,帮我们干这些重活儿,并进行大量

  • vue渲染方式render和template的区别

    render函数详解 Vue中的Render函数中有一个参数,这个参数是一个函数通常我们叫做h.其实这个h叫做createElement.Render函数将createElement的返回值放到了HTML中 createElement这个函数中有3个参数 第一个参数(必要参数):主要用于提供DOM的html内容,类型可以是字符串.对象或函数 第二个参数(类型是对象,可选):用于设置这个DOM的一些样式.属性.传的组件的参数.绑定事件之类 第三个参数(类型是数组,数组元素类型是VNode,可选):

  • 谈谈React中的Render Props模式

    概述 Render Props模式是一种非常灵活复用性非常高的模式,它可以把特定行为或功能封装成一个组件,提供给其他组件使用让其他组件拥有这样的能力,接下来我们一步一步来看React组件中如何实现这样的功能. 简要介绍:分离UI与业务的方法一直在演进,从早期的mixins,到HOC,再到Render Prop,本文主要对比HOC,谈谈Render Props 1 . 早期的mixins 早期复用业务通过mixins来实现,比如组件A和组件B中,有一些公用函数,通过mixins剥离这些公用部分,并

  • vue+render+jsx实现可编辑动态多级表头table的实例代码

    最近项目需要实现可编辑的动态多级表头表格,看了两天的文章,始终没有找到我想要的效果,在了解了render+jsx的基础用法后,自己基于element-ui封装了一个,数据格式参考element-ui table的数据.实现如下: 1.scoresTable <script> import scoresColumn from "./scoresColumn"; export default { components: { scoresColumn }, render: fun

  • Unity使用LineRender实现绘画功能

    本文实例为大家分享了Unity LineRender实现绘画功能的具体代码,供大家参考,具体内容如下 老规矩,直接上代码: using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class DrawLine_ZH : MonoBehaviour { private GameObject _Clone; private LineRenderer _Lin

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

    简而言之,只要一个组件中某个属性的值是函数,那么就可以说该组件使用了 Render Props 这种技术.听起来好像就那么回事儿,那到底 Render Props 有哪些应用场景呢,咱们还是从简单的例子讲起,假如咱们要实现一个展示个人信息的组件,一开始可能会这么实现: const PersonInfo = props => ( <div> <h1>姓名:{props.name}</h1> </div> ); // 调用 <PersonInfo n

  • JS动画实现回调地狱promise的实例代码详解

    1. js实现动画 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>animate</title> <style> .ball { width: 40px; height: 40px; margin-bottom: 5px; border-radius: 20px; } .ball1 { ba

  • JavaScript详解使用Promise处理回调地狱与async await修饰符

    目录 Promise 回调地狱 Promise简介 Promise简单使用 async和await 修饰符 小结 Promise Promise能够处理异步程序. 回调地狱 JS中或node中,都大量的使用了回调函数进行异步操作,而异步操作什么时候返回结果是不可控的,如果我们希望几个异步请求按照顺序来执行,那么就需要将这些异步操作嵌套起来,嵌套的层数特别多,就会形成回调地狱 或者叫做 横向金字塔. 案例:有a.txt.b.txt.c.txt三个文件,使用fs模板按照顺序来读取里面的内容,代码:

  • 在vue项目中promise解决回调地狱和并发请求的问题

    场景需求: 需要同时请求5个接口 都请求成功后执行下一步操作 解决方法: 定义一个变量i=5,请求成功一个接口,让i–,直到i=0时执行下一个操作,否则不执行 axios.all 并发请求,.then(axios.spread(function(callback1, callback2)){}) promise.all 并发请求,.then(function([callback1, callback2]){}) 1.回调地狱: 函数作为参数层层嵌套 代替的为.then的链式操作 2.promis

  • Vue Promise解决回调地狱问题实现方法

    目录 问题 解决方案 问题 首先,什么是回调地狱: 层嵌套的问题. 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性. 当一个接口需要依赖另一个接口的请求数据时,通常有两种解决方式 将请求数据的接口设为同步,之后调另一个接口 在请求数据接口的成功回调里调另一个接口 这两种问题在回调函数时代尤为突出.Promise 的诞生就是为了解决这两个问题. 典型的高阶函数,将回调函数作为函数参数传给了readFile.但久而久之,就会发现,这种传入回调的方式也

  • React Render Props共享代码技术

    目录 介绍 使用 使用children prop代替render prop Render props 与 React.PureComponent 同时使用 注意 解决方案 介绍 Render Props是指在React组件之间使用一个值为函数的prop共享代码的技术 具有render prop的组件接受一个返回React元素的函数,并在组件内部通过调用此函数来实现自己的渲染逻辑 主要用于抽离逻辑,进行代码复用 使用 以官方文档的Mouse & Cat示例为例 创建Mouse组件,此组件仅用于提供

  • JavaScript中异步与回调的基本概念及回调地狱现象

    目录 JavaScript异步与回调 一.前言 二.异步函数 三.回调函数 四.回调的回调 五.回调地狱 六.总结 JavaScript异步与回调 一.前言 在学习本文内容之前,我们必须要先了解异步的概念,首先要强调的是异步和并行有着本质的区别. 并行,一般指并行计算,是说同一时刻有多条指令同时被执行,这些指令可能执行于同一CPU的多核上,或者多个CPU上,或者多个物理主机甚至多个网络中. 同步,一般指按照预定的顺序依次执行任务,只有当上一个任务完成后,才开始执行下一个任务. 异步,与同步相对应

  • react render props模式实现组件复用示例

    目录 一 render props的使用步骤 二 组件的复用 三 使用children名代替属性 一 render props的使用步骤 1 创建要复用的组件,在组件中提供要复用的状态逻辑代码2 将要复用的state作为方法的参数,暴露到组件外部 import React from "react"; import ReactDOM from "react-dom"; class App extends React.Component { render() { ret

随机推荐