要问程序员同学们尤为著名的模式是什么,当然是在React 库实现 DRY 高阶组件,进阶文章来了,要准备好哦~
在你听到 Don't Repeat Yourself或者 D.R.Y 这样(中邪一样)的口号之前你是不会在软件开发的钻研之路上走得很远的。有时候实行这些名言会有点过于麻烦,但是在大多数情况下,(实行它)是一个有价值的目标。在这篇文章中我们将会去探讨在 React 库中实现 DRY 的尤为著名的模式——高阶组件。不过在我们探索答案之前,我们首先必须要完全明确问题来源。
假设我们要负责重新创建一个类似于 Sprite(译者注:国外的一个在线支付公司)的仪表盘。正如大多数项目那样,一切事务在最后收尾之前都工作得很正常。你发现在仪表盘上有一串不一样的提示框需要你某些元素 hover 的时候显示。 => 你在仪表盘上面发现了一些不同的、(当鼠标)悬停在某些组成元素上面会出现的提示信息。
![1541493221792_DRY 的最著名的模式——高阶组件.gif](http://www.itheima.com/images/newslistPIC/1541493221792_DRY 的最著名的模式——高阶组件.gif)
这里有好几种方式可以实现这个效果。其中一个你可能想到的是监听特定的组件的 hover 状态来决定是否展示 tooltip。在上图中,你有三个组件需要添加它们的监听功能—— Info、TrendChart 和 DailyChart。
让我们从 Info 组件开始。现在它只是一个简单的 SVG 图标。
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> ) }}复制代码现在我们需要添加让它可以监测到自身是否被(鼠标)悬停的功能。我们可以使用 React 所附带的 onMouseOver 和 onMouseOut 这两个鼠标时间。我们传递给 onMouseOver 的函数将会在组件被鼠标悬停后触发,同时我们传递给 onMouseOut 的函数将会在组件不再被鼠标悬停时触发。要以 React 的方式来操作,我们会给给我们的组件添加一个 hovering state 属性,所以我们可以在 hovering state 属性改变的时候触发重绘,来展示或者隐藏我们的提示框。
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。如果这两个组件没有出问题,就请不要修复它。我们对于 Info 的悬停功能运行的很好,所以请再写一遍之前的代码。
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} /> </> ) }}复制代码你或许知道下一步了:我们要对最后一个组件 DailyChart 做同样的事情。
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} /> </> ) }}复制代码这样的话,我们就全部做完了。你可能以前曾经这样写过 React 代码。但这并不该是你最终所该做的(不过这样做也还凑合),但是它很不 “DRY”。正如我们所看到的,我们在我们的每一个组件中都 重复着完全一样的的鼠标悬停逻辑。
从这点看的话,问题变得非常清晰了:我们希望避免在在每个需要添加鼠标悬停逻辑的组件是都再写一遍相同的逻辑。所以,解决办法是什么?在我们开始前,让我们先讨论一些能让我们更容易理解答案的编程思想—— 回调函数 和 高阶函数。
在 JavaScript 中,函数是 “一等公民”。这意味着它就像对象/数组/字符串那样可以被声明为一个变量、当作函数的参数或者在函数中返回一个函数,即使返回的是其他函数也可以。
function add (x, y) { return x + y}function addFive (x, addReference) { return addReference(x, 5)}addFive(10, add) // 15复制代码如果你没这样用过,你可能会感到困惑。我们将 add 函数作为一个参数传入 addFive 函数,重新命名为 addReference,然后我们调用了着个函数。
这时候,你作为参数所传递进去的函数被叫做回调函数同时你使用回调函数所构建的新函数被叫做高阶函数。
因为这些名词很重要,下面是一份根据它们所表示的含义重新命名变量后的同样逻辑的代码。
function add (x,y) { return x + y}function higherOrderFunction (x, callback) { return callback(x, 5)}higherOrderFunction(10, add)复制代码这个模式很常见,哪里都有它。如果你之前用过任何 JavaScript 数组方法、jQuery 或者是 lodash 这类的库,你就已经用过高阶函数和回调函数了。
[1,2,3].map((i) => i + 5)_.filter([1,2,3,4], (n) => n % 2 === 0 );$('#btn').on('click', () => console.log('回调函数哪里都有'))复制代码让我们回到我们之前的例子。如果我们不仅仅想创建一个 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) // 15addTen(10, add) // 20addTwenty(10, add) // 30复制代码再一次出现这种情况,这样写并不糟糕,但是我们重复写了好多相似的逻辑。这里我们的目标是要能根据需要写很多 “adder” 函数(addFive、addTen、addTwenty 等等),同时尽可能减少代码重复。为了完成这个目标,我们创建一个 makeAdder 函数怎么样?着个函数可以传入一个数字和原始 add 函数。因为这个函数的目的是创建一个新的 adder 函数,我们可以让其返回一个全新的传递数字来实现加法的函数。这儿讲的有点多,让我们来看下代码吧。
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) // 15addTen(10) // 20addTwenty(10) // 30复制代码太酷了!现在我们可以在需要的时候随意地用最低的代码重复度创建 “adder” 函数。
如果你在意的话,这个通过一个多参数的函数来返回一个具有较少参数的函数的模式被叫做 “部分应用(Partial Application)“,它也是函数式编程的技术。JavaScript 内置的 “.bind“ 方法也是一个类似的例子。
好吧,那这与 React 以及我们之前遇到鼠标悬停的组件有什么关系呢?我们刚刚通过创建了我们的 makeAdder 这个高阶函数来实现了代码复用,那我们也可以创建一个类似的 “高阶组件” 来帮助我们实现相同的功能(代码复用)。不过,不像高阶函数返回一个新的函数那样,高阶组件返回一个新的组件来渲染 “回调” 组件
作者:
黑马程序员前端与移动开发培训学院首发:
http://web.itheima.com/?v2