[译] React 高级话题之 Render Props

  发布时间:   文章分类:IOS Android 浏览数: 19

文章来源: juejin.im/post/5bfe6b05f265da6......, 本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

本文为意译,翻译过程中掺杂本人的理解,如有误导,请放弃继续阅读。

原文地址:Render Props

render prop 是一个技术概念。它指的是使用值为function类型的prop来实现React component之间的代码共享。

如果一个组件有一个render属性,并且这个render属性的值为一个返回React element的函数,并且在组件内部的渲染逻辑是通过调用这个函数来完成的。那么,我们就说这个组件使用了 render props 技术。

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>
复制代码

不少类库都使用了这种技术,比如说:React Router和 Downshift

在这个文档里面,我们将会讨论为什么render props是如此有用,你改如何编写自己的render props 组件。

正文

使用Render Props来完成关注点分离

在React中,组件是代码复用的基本单元(又来了,官方文档不断地在强调这个准则)。到目前为止,在React社区里面,关于共享state或者某些相似的行为(比如说,将一个组件封装进另一拥有相同state的组件)还没有一个明朗的方案。

举个例子,下面这个组件是用于在web应用中追踪鼠标的位置:

class MouseTracker extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <h1>Move the mouse around!</h1>
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

复制代码

随着光标在屏幕上面移动,这个组件将会在文档的

标签里面显示当前光标在x,y轴上的坐标值。

那么问题来了: 我们该如何在别的组件复用这种行为(指的是监听mouseMove事件,获取光标的坐标值)呢?换句话说,如果别的组件也需要知道目前光标的坐标值,那我们能不能将这种行为封装好,然后在另外一个组件里面开箱即用呢?

因为,在React中,组件是代码复用的基本单元(again)。那好,我们一起来重构一下代码,把我们需要复用的行为封装到Mouse组件当中。

// The <Mouse> component encapsulates the behavior we need...
class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/* ...but how do we render something other than a <p>? */}
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse />
      </div>
    );
  }
}
复制代码

现在,Mouse 组件看似把所有跟监听mousemove事件,保存光标的坐标值等相关的行为封装在一起了。实际上,它还不能达到真正的可复用。

假设,我们需要实现这么一个组件。它需要渲染出一只用图片表示的猫去追逐光标在屏幕上移动的视觉效果。我们可能会通过向Cat组件传递一个叫mouse(它的值为{{x,y}})的prop来获得当前光标所在位置所对应的坐标。

首先,我们会在Mouse组件的render方法里面插入这个Cat组件,像这样子:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class MouseWithCat extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          We could just swap out the <p> for a <Cat> here ... but then
          we would need to create a separate <MouseWithSomethingElse>
          component every time we need to use it, so <MouseWithCat>
          isn't really reusable yet.
        */}
        <Cat mouse={this.state} />
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <MouseWithCat />
      </div>
    );
  }
}
复制代码

这种方式的实现可能对个别的场景有用,但是,我们没有达成通过封装让这种行为真正地复用的目标。在别的应用场景下,每一次当我们需要获取光标在屏幕上的坐标的时候,我们都需要重新创建一个组件(例如,一个跟MouseWithCat相似组件)来实现这个业务场景所对应的渲染任务。

这个时候,就轮到render props 出场啦:相比直接把Cat这个组件硬编码到Mouse组件当中,刻意地去改变Mouse组件的UI输出(也就是我们重新定义一个MouseWithCat组件的原因),更好的做法是,我们可以给Mouse组件定义一个值是函数类型的prop,让这个prop自己来动态地决定要在Mouse组件的render方法要渲染东西。这个值为函数类型的prop就是我们所说的render prop了。

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}
复制代码

现在,相比每一次重复地将Mouse组件的代码复制一遍,然后将我们要渲染的东西硬编码到Mouse的render方法中去,我们采取了一个更省力的办法。那就是给Mouse新增了一个render属性,让这个属性来决定要在Mouse组件中渲染什么。

更加具体和直白地说, 一个render prop(这里不是代指技术,而是组件属性) 就是一个值为函数类型的prop。通过这个函数,我们让挂载了这个prop的组件知道自己要去渲染什么

这种技术使得我们之前想要共享的某些行为变得非常之可移植(portable)。假如你想要得到这种行为,你只需要渲染一个带render属性的类Mouse组件到你的组件树当中就可以了。剩下的就让这个render prop来获取相关的数据(通过函数参数实例化时得到。拿上述例子来说,就是 (mouse)=> <Cat mouse={mouse}>mouse ),然后决定如何干预这个组件的渲染。

一个很有意思的,并值得我们注意的事情是,你完全可以通过一个带render属性的普通组件来实现大部分的HOC。举个例子,假如你在共享行为(监听mousemove事件,获得光标在屏幕上的坐标)时不想通过Mouse组件来完成,而是想通过高阶组件withMouse来完成的话,那么就可以很简单地通过创建一个带render prop的Mouse组件来达成:

// If you really want a HOC for some reason, you can easily
// create one using a regular component with a render prop!
function withMouse(Component) {
  return class extends React.Component {
    render() {
      return (
        <Mouse render={mouse => (
          <Component {...this.props} mouse={mouse} />
        )}/>
      );
    }
  }
}
复制代码

可以这么说,render prop(指技术)让HOC技术与其他技术(在这里,指它自己)的组合使用成为了可能。

render prop 的prop名不一定叫“render”

如上面的标题,你要牢牢记住,这种技术虽然叫“render props”,但是prop属性的名称不一定非得叫“render”。实际上,只要组件上的某个属性值是函数类型的,并且这个函数通过自己的形参实例化时获取了这个组件的内部数据,参与到这个组件的UI渲染中去了,我们就说这个组件的实现应用了 render props 这种技术。

上面的例子当中,我们一直在使用“render”这个名称,实际上,我们也可以轻易地换成 children 这个名称!

<Mouse children={mouse => (
  <p>The mouse position is {mouse.x}, {mouse.y}</p>
)}/>
复制代码

同时,我们也要记住,这个“children”prop不一定非得罗列在在JSX element的“属性”列表中。它实际上就是我们平时用JSX声明组件时的children,因此你也可以像以前一样把它放在组件的children所在的位置。

<Mouse>
  {mouse => (
    <p>The mouse position is {mouse.x}, {mouse.y}</p>
  )}
</Mouse>
复制代码

react-motion 这个库的API的话,你会看到这种技术的应用。

因为这种技术比较少见。假如你这么做了,为了让看你代码的人不产生疑惑的话,你可能需要在静态属性propTypes中显式地声明一下children的数据类型必须为函数。

Mouse.propTypes = {
  children: PropTypes.func.isRequired
};
复制代码

注意点

当跟React.PureComponent结合使用时,要当心

如果你在组件的render方法里面创建了一个函数的话,然后把这个函数复制给一个组件的prop的话,那么收获的结果很有可能是违背你初衷的。怎么说呢?因为在做shallow prop comparison的时候,new prop都会被判断为不等于old prop的。而你一旦这么做了的话(指第一句话),恰恰会在每一次render的调用的时候生成一个新的值给这个属性。

我们继续拿上面的Mouse组件作为例子。假如Mouse 组件继承了React.PureComponent的话,我们的代码应该是像下面你这样的:

class Mouse extends React.PureComponent {
  // Same implementation as above...
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>

        {/*
          This is bad! The value of the `render` prop will
          be different on each render.
        */}
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}
复制代码

在上面的代码例子当中,每一次MouseTracker的render方法被调用的时候,它都会生成一个新的函数实例给Mouse组件,作为“render”属性的值。然而,我们之所以继承React.PureComponent,就是想减少Mouse组件被渲染的次数。如此一来,Mouse因为一个新的函数实例被迫判定为props已经发生改变了,于是乎进行了不必要的渲染。这与我们的让Mouse组件继承React.PureComponent的初衷是相违背的。

为了避开(To get around)这个问题,你可以把render prop的值复制为组件实例的方法,这样:

class MouseTracker extends React.Component {
  // Defined as an instance method, `this.renderTheCat` always
  // refers to *same* function when we use it in render
  renderTheCat(mouse) {
    return <Cat mouse={mouse} />;
  }

  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={this.renderTheCat} />
      </div>
    );
  }
}
复制代码

在某些场景下,你可能无法把prop的值静态地赋值为组件实例的某个方法(e.g. because you need to close over the component’s props and/or state)。那么,这种情况下,你只能老老实实地让Mouse组件去继承React.Component了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 Coder·码农网

码农可能感兴趣的文章:

本文永久链接:www.codercto.com/a/42123.html

相关码农书籍:
数据结构与算法分析

数据结构与算法分析

Frank.M.Carrano / 金名 / 清华大学出版社 / 2007-11 / 98.00元

“数据结构”是计算机专业的基础与核心课程之一,Java是现今一种热门的语言。本书在编写过程中特别考虑到了面向对象程序设计(OOP)的思想与Java语言的特性...

相关码农工具:
HSV CMYK 转换工具

HSV CMYK 转换工具

HSV CMYK互换工具

RGB转16进制工具

RGB转16进制工具

RGB HEX 互转工具

随机密码生成器

随机密码生成器

多种字符组合密码