深刻浅出理解 React高阶组件

开始以前,有两点须要说明一下:一、React 高阶组件 仅仅是一种模式,并非 React 的基础知识;二、它不是开发 React app 的必要知识。你能够略过此文章,仍然能够开发 React app。然而,技多不压身,若是你也是一位 React 开发者,强烈建议你掌握它。javascript

1、为何须要高阶组件

若是你不知道 Don't Repeat YourselfD.R.Y,那么在软件开发中一定走不太远。对于大多数开发者来讲,它是一个开发准则。在这篇文章当中,咱们将了解到如何在 React 当中运用 DRY 原则 —— 高阶组件。开始阐述以前,咱们先来认识一下问题所在。java

假设咱们要开发相似下图的功能。正如大多的项目同样,咱们先按流程开发着。当开发到差很少的时候,你会发现页面上有不少,鼠标悬浮在某个元素上出现 tooltip 的场景。react

图片

有不少种方法作到这样。你可能想到写一个带悬浮状态的组件来控制 tooltip 的显示与否。那么你须要添加三个组件——Info, TrendChart 和 DailyChart。编程

咱们从 Info 组件开始。它很简单,仅仅是一个 SVG icon.数组

class Info extends React.Component {
  render() {
    return (
      <svg className="Icon-svg Icon--hoverable-svg" height={this.props.height} viewBox="0 0 16 16" width="16" > <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" /> </svg> ); } } 复制代码

而后咱们须要添加一个状态来记录组件是否被 Hover,能够用 React 鼠标事件当中的 onMouseOveronMouseOut来实现。app

class Info extends React.Component {
  state = { hovering: false };
  mouseOver = () => this.setState({ hovering: true });
  mouseOut = () => this.setState({ hovering: false });
  render() {
    return (
      <>
        {this.state.hovering === true ? <Tooltip id={this.props.id} /> : null}
        <svg
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
          className="Icon-svg Icon--hoverable-svg"
          height={this.props.height}
          viewBox="0 0 16 16"
          width="16"
        >
          <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
        </svg>
      </>
    );
  }
}
复制代码

看起来还不错,咱们须要在 TrendChartDailyChart写一样的逻辑。svg

class TrendChart extends React.Component {
  state = { hovering: false };
  mouseOver = () => this.setState({ hovering: true });
  mouseOut = () => this.setState({ hovering: false });
  render() {
    return (
      <>
        {this.state.hovering === true ? <Tooltip id={this.props.id} /> : null}
        <Chart
          type="trend"
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        />
      </>
    );
  }
}
复制代码
class DailyChart extends React.Component {
  state = { hovering: false };
  mouseOver = () => this.setState({ hovering: true });
  mouseOut = () => this.setState({ hovering: false });
  render() {
    return (
      <>
        {this.state.hovering === true ? <Tooltip id={this.props.id} /> : null}
        <Chart
          type="daily"
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        />
      </>
    );
  }
}
复制代码

三个组件咱们都开发完成。但正如你看到的,很是不 DRY ,由于咱们在三个组件中把同一套 hover 逻辑 重复了三次。函数

问题就显而易见了。当一个新组件须要相似 hover 逻辑 时,咱们应避免重复。那么,咱们该如何解决呢?为了便于理解,先来了解一下编程当中的两个概念—— 回调高阶函数学习

2、什么是回调和高阶函数

在 JavaScript 当中,函数是第一公民。也就是说它能够像 objects/arrays/strings 被赋值给变量、被看成参数传递给函数和被函数返回。ui

function add(x, y) {
  return x + y;
}

function addFive(x, addReference) {
  return addReference(x, 5);
}

addFive(10, add); // 15
复制代码

你可能会感到有点儿绕:咱们在 函数addFive 中传入一个函数名为 addReference 的参数,而且在内部返回时调用它。相似这种状况,你把它看成参数传递的函数叫 回调;接收函数做为参数的函数叫 高阶函数

为了更直观,咱们把上述代码的命名概念化。

function add(x, y) {
  return x + y;
}

function higherOrderFunction(x, callback) {
  return callback(x, 5);
}

higherOrderFunction(10, add);
复制代码

这种写法其实很常见。若是你用过数组方法、jQuery 或 lodash 库,那么你就使用过 回调 和 高阶函数。

[1, 2, 3].map(i => i + 5);

_.filter([1, 2, 3, 4], n => n % 2 === 0);

$("#btn").on("click", () => console.log("Callbacks are everywhere"));
复制代码

3、高阶函数的简单应用

回到以前写的那个例子。咱们不只须要 addFive,可能还需 addTen addTwenty等等。依照如今的写法,当咱们写一个新函数的时候,不得不重复原有逻辑。

function add(x, y) {
  return x + y;
}

function addFive(x, addReference) {
  return addReference(x, 5);
}

function addTen(x, addReference) {
  return addReference(x, 10);
}

function addTwenty(x, addReference) {
  return addReference(x, 20);
}

addFive(10, add); // 15
addTen(10, add); // 20
addTwenty(10, add); // 30
复制代码

看起来还不错,但仍然有点重复。咱们的目的是用更少的代码建立更多的 adder函数(addFive, addTen, addTwenty 等等)。鉴于此,咱们建立一个makeAdder函数 ,此函数接收一个 数字 和 一个函数 做为参数,长话少说,直接看代码。

function add(x, y) {
  return x + y;
}

function makeAdder(x, addReference) {
  return function(y) {
    return addReference(x, y);
  };
}

const addFive = makeAdder(5, add);
const addTen = makeAdder(10, add);
const addTwenty = makeAdder(20, add);

addFive(10); // 15
addTen(10); // 20
addTwenty(10); // 30
复制代码

很好,如今咱们想要多少 adder函数 就能写多少,而且不必写那么多重复代码。

这种使用一个函数并将其应用一个或多个参数,但不是所有参数,在这个过程当中建立并返回一个新函数叫『偏函数应用』。 JavaScript 当中的 .bind即是这种方法的一个例子。

4、高阶组件

那么,这些和咱们最初写 React 代码重复又有什么关系呢?也像建立 高阶函数makeAdder 同样地建立相似 高阶组件 。看起来还不错,咱们试试吧。

高阶函数

  • 一个函数
  • 接收一个回调函数为参数
  • 返回一个新的函数
  • 返回的函数能够调用传进去的回调函数
function higherOrderFunction(callback) {
  return function() {
    return callback();
  };
}
复制代码

高阶组件

  • 一个组件
  • 接收一个组件为参数
  • 返回一个新的组件
  • 返回的组件能够渲染当初传进去的组件
function higherOrderComponent(Component) {
  return class extends React.Component {
    render() {
      return <Component />; } }; } 复制代码

5、高阶组件的简单应用

好,咱们如今理解了高阶组件的基本概念。你应该还记得,最初面临的问题是在太多地方重复了 Hover 逻辑 部分。

state = { hovering: false };
mouseOver = () => this.setState({ hovering: true });
mouseOut = () => this.setState({ hovering: false });
复制代码

记住,咱们但愿高阶组件(命名为 withHover)能压缩 Hover 逻辑 部分,并带有 hovering 状态,这样能避免咱们重复 Hover 逻辑。

最终目标,不管什么时候咱们想写一个带 Hover 状态的组件时,均可以把这个组件做为参数传入咱们的高阶组件 withHover

const InfoWithHover = withHover(Info);
const TrendChartWithHover = withHover(TrendChart);
const DailyChartWithHover = withHover(DailyChart);
复制代码

接着,不管什么组件传入 withHover ,都会返回组件自己,而且会接收一个 hovering 属性。

function Info({ hovering, height }) {
  return (
    <>
      {hovering === true ? <Tooltip id={this.props.id} /> : null}
      <svg
        className="Icon-svg Icon--hoverable-svg"
        height={height}
        viewBox="0 0 16 16"
        width="16"
      >
        <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
      </svg>
    </>
  );
}
复制代码

如今,咱们须要开始写 withHover组件 了。正如以上,须要作到如下三点:

  • 接收一个『组件』为参数
  • 返回一个新的组件
  • 参数组件接收一个 “hovering” 属性

一、接收一个『组件』为参数

function withHover(Component) {}
复制代码

二、返回一个新的组件

function withHover(Component) {
  return class WithHover extends React.Component {};
}
复制代码

三、参数组件接收一个 “hovering” 属性

新问题来了, hovering 该从哪里来?咱们能够建立一个新的组件,把 hovering 看成该组件的状态,而后传给最初的那个参数组件。

function withHover(Component) {
  return class WithHover extends React.Component {
    state = { hovering: false };
    mouseOver = () => this.setState({ hovering: true });
    mouseOut = () => this.setState({ hovering: false });
    render() {
      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}> <Component hovering={this.state.hovering} /> </div> ); } }; } 复制代码

我想起了一句话:组件是把 props 转换成 UI 的过程;高阶组件是把一个组件转换成另外一个组件的过程。

咱们已经学习完了高阶函数的基础知识,但仍然有几点值得讨论。

6、高阶组件的进阶应用

回头看看组件 withHover ,仍是有一点不足:就是它假想了用户传进去的参数组件必需要接收一个名为 hovering 的 prop;若是参数组件自己就有一个名为 hovering 的 prop,而且这个 prop 并非来处理 hover 的, 就会形成命名冲突。咱们能够尝试一下让用户自定义控制 hover 的 prop 命名。

function withHover(Component, propName = "hovering") {
  return class WithHover extends React.Component {
    state = { hovering: false };
    mouseOver = () => this.setState({ hovering: true });
    mouseOut = () => this.setState({ hovering: false });
    render() {
      const props = {
        [propName]: this.state.hovering
      };

      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}> <Component {...props} /> </div> ); } }; } 复制代码

在 withHover 中,咱们给 propName 设定了一个默认值 hovering,用户也能够在组件中传入第二个参数自定义命名。

function withHover(Component, propName = "hovering") {
  return class WithHover extends React.Component {
    state = { hovering: false };
    mouseOver = () => this.setState({ hovering: true });
    mouseOut = () => this.setState({ hovering: false });
    render() {
      const props = {
        [propName]: this.state.hovering
      };

      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
          <Component {...props} />
        </div>
      );
    }
  };
}

function Info({ showTooltip, height }) {
  return (
    <>
      {showTooltip === true ? <Tooltip id={this.props.id} /> : null}
      <svg
        className="Icon-svg Icon--hoverable-svg"
        height={height}
        viewBox="0 0 16 16"
        width="16"
      >
        <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
      </svg>
    </>
  );
}

const InfoWithHover = withHover(Info, "showTooltip");
复制代码

你可能又注意到了另一个问题,在组件 Info 中,它还接收一个名为 height 的 prop。按照如今这种写法,height 只能是 undefined,但咱们指望能达到以下效果:

const InfoWithHover = withHover(Info)

...

return <InfoWithHover height="16px" />
复制代码

咱们把 height 传入 InfoWithHover ,可是该如何使它生效呢?

function withHover(Component, propName = "hovering") {
  return class WithHover extends React.Component {
    state = { hovering: false };
    mouseOver = () => this.setState({ hovering: true });
    mouseOut = () => this.setState({ hovering: false });
    render() {
      console.log(this.props); // { height: "16px" }

      const props = {
        [propName]: this.state.hovering
      };

      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}> <Component {...props} /> </div> ); } }; } 复制代码

从 console 中能够看出, this.props 的值是 { height: "16px" } 。咱们要作的就是无论 this.props 为什么值,都把 它传给参数组件 Component

render() {
      const props = {
        [propName]: this.state.hovering,
        ...this.props,
      }

      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}> <Component {...props} /> </div> ); } 复制代码

最终,咱们能够看出,经过使用高阶组件能够有效地复用同套逻辑,避免过多的重复代码。可是,它真的没有任何缺点吗?显然不是。

7、高阶组件的小瑕疵

当咱们使用高阶组件的时候,可能会发生 inversion of control(控制反转) 。想象一下,假如咱们正使用 React Router 的 withRouter ,根据文档:不管是什么组件,它都会把 match, locationhistory 传给该组件的 prop。

class Game extends React.Component {
  render() {
    const { match, location, history } = this.props // From React Router

    ...
  }
}

export default withRouter(Game)
复制代码

从上能够看出,若是咱们的组件 Game 也有命名为 match, locationhistory 的 prop 时,便会引起命名冲突。这个问题,咱们在写组件 withHover 遇到过,并经过传入第二参数自定义命名的方式解决了该问题。可是当咱们用到第三方库中的高阶组件时,就不必定会有那么幸运了。咱们不得不修改咱们自身组件 prop 的命名 或 中止使用第三方库中的该高阶组件。

8、结尾

本文是翻译自 [React Higher-Order Components](React Higher-Order Components),仅供学习参考。若是给您学习理解形成了迷惑,欢迎联系我。

相关文章
相关标签/搜索