开始以前,有两点须要说明一下:一、React 高阶组件 仅仅是一种模式,并非 React 的基础知识;二、它不是开发 React app 的必要知识。你能够略过此文章,仍然能够开发 React app。然而,技多不压身,若是你也是一位 React 开发者,强烈建议你掌握它。javascript
若是你不知道 Don't Repeat Yourself
或 D.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 鼠标事件当中的 onMouseOver
和 onMouseOut
来实现。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>
</>
);
}
}
复制代码
看起来还不错,咱们须要在 TrendChart
和 DailyChart
写一样的逻辑。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 逻辑 时,咱们应避免重复。那么,咱们该如何解决呢?为了便于理解,先来了解一下编程当中的两个概念—— 回调 和 高阶函数。学习
在 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"));
复制代码
回到以前写的那个例子。咱们不只须要 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
即是这种方法的一个例子。
那么,这些和咱们最初写 React 代码重复又有什么关系呢?也像建立 高阶函数makeAdder
同样地建立相似 高阶组件
。看起来还不错,咱们试试吧。
高阶函数
function higherOrderFunction(callback) {
return function() {
return callback();
};
}
复制代码
高阶组件
function higherOrderComponent(Component) {
return class extends React.Component {
render() {
return <Component />; } }; } 复制代码
好,咱们如今理解了高阶组件的基本概念。你应该还记得,最初面临的问题是在太多地方重复了 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组件
了。正如以上,须要作到如下三点:
一、接收一个『组件』为参数
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 的过程;高阶组件是把一个组件转换成另外一个组件的过程。
咱们已经学习完了高阶函数的基础知识,但仍然有几点值得讨论。
回头看看组件 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> ); } 复制代码
最终,咱们能够看出,经过使用高阶组件能够有效地复用同套逻辑,避免过多的重复代码。可是,它真的没有任何缺点吗?显然不是。
当咱们使用高阶组件的时候,可能会发生 inversion of control(控制反转)
。想象一下,假如咱们正使用 React Router 的 withRouter
,根据文档:不管是什么组件,它都会把 match
, location
和history
传给该组件的 prop。
class Game extends React.Component {
render() {
const { match, location, history } = this.props // From React Router
...
}
}
export default withRouter(Game)
复制代码
从上能够看出,若是咱们的组件 Game
也有命名为 match
, location
和history
的 prop 时,便会引起命名冲突。这个问题,咱们在写组件 withHover
遇到过,并经过传入第二参数自定义命名的方式解决了该问题。可是当咱们用到第三方库中的高阶组件时,就不必定会有那么幸运了。咱们不得不修改咱们自身组件 prop 的命名 或 中止使用第三方库中的该高阶组件。
本文是翻译自 [React Higher-Order Components](React Higher-Order Components),仅供学习参考。若是给您学习理解形成了迷惑,欢迎联系我。