使用React的函数式编程的基础知识

语言: CN / TW / HK

理解函数式编程的概念是React开发者的一项宝贵技能。这是一个重要的话题,大多数React初学者经常忽略,使他们在理解React如何做出一些决定时遇到了问题。

在这篇文章中,我们将介绍函数式编程的概念以及React如何采用它来编写更容易测试和维护的应用程序。

要学习本教程,请确保你对React有基本的了解。

函数式编程的快速概述

我们编写的每个程序或应用程序都遵循一种方法或写作风格,也就是所谓的范式。因此,函数式编程是一种声明式的编程范式,程序是由纯函数组成的。

让我们注意一下 "组合 "和 "纯 "这两个词,因为它们构成了函数式编程的基石,我们将在下面的章节中讨论它们。

数学中的函数

为了更好地理解函数式编程的概念,让我们快速浏览一下数学中常见的函数。例如,我们有一个给定的函数。

``` y = f(x)

```

在这个函数中,输出,y ,只对输入,x 进行计算。这意味着每次我们用相同的输入x ,调用这个函数时,我们总是得到相同的输出,y

该函数不影响自身以外的任何东西,也不修改传入的输入。因此,它被称为一个确定性的或纯函数

让我们看一个例子。

``` y = f(x) = 4x // if x = 2, y = 4(2) = 8

```

如上所见,对于每一个输入,x = 2 ,输出y ,将永远是8 。像这样的函数总是更容易理解和推理,因为我们确切地知道我们所期望的东西。

让我们再往前走一步,写一个更复杂的这样的函数。

``` z = c(f(x))

```

在这里,我们有两个函数,cf ,它们被组合在一起形成一个更复杂的函数。在数学中,我们说cf(x) 的一个函数,这意味着我们必须首先单独评估f(x) ,像这样。

``` y = f(x)

```

然后,我们将结果作为参数传递给c ,像这样。

``` z = c(y)

```

这种功能概念被称为函数组合。它鼓励代码的可重用性和可维护性。

有了这个数学概念,理解计算机科学编程中的函数式概念就是小菜一碟了。

React中的函数式编程

接下来,我们将解释函数式编程概念是如何在React中应用的。我们还将看到JavaScript中的例子,因为它是React的底层语言。

让我们先看一下下面的JavaScript代码。

``` const arr = [2, 4, 6];

function addElement(arr, ele) { arr.push(ele); }

addElement(arr, 8);

console.log("original data", arr); // Expected Output: [2, 4, 6, 8]

```

在这里,我们定义了一个名为addElement 的函数,向一个数组添加一个元素。该代码的工作原理如输出所示,你可以 在CodeSandbox上 自己尝试一下

这段代码看起来与前面解释的数学中的函数概念相似。也就是说,一个函数只对输入参数进行操作,以创建一个输出。

但仔细观察这段代码,我们会发现它违反了一个纯粹的函数概念,即一个函数不应影响它以外的任何东西,也不应修改传入的参数。

一个这样做的函数是一个不纯的函数,并且有副作用,例如操纵输入参数,在本例中,全局arr 数组。

在代码中,全局arr 数组被突变了,也就是说,函数将全局变量从最初的[2,4,6] 改为[2,4,6,8]

现在,想象一下我们想重新使用这个全局变量来组成另一个函数。如果我们继续这样做,我们会得到一个非预期的输出,可能会导致我们的程序出现错误。

这就把我们带到了函数式编程的第一个信条:纯函数。

使用纯函数

在函数式编程中,我们编写纯函数,也就是只在输入值上返回输出的函数,从不影响它们之外的任何东西。这有助于防止我们的代码中出现错误。

将这个函数式概念应用到上面的代码中,我们可以得到以下结果。

``` const arr = [2, 4, 6];

function addElement(arr, ele) { return [...arr, ele]; }

console.log("modified data", addElement(arr, 8)); // Expected Output: [2, 4, 6, 8] console.log("original data", arr); // Expected Output: [2, 4, 6]

```

上面的函数是纯粹的,它只在输入参数上计算输出,从不改变全局arr ,我们可以从结果中看到。

你可以在CodeSandbox上自己尝试一下

请注意,这段代码也使用了不变性的概念来使函数变得纯粹(我们稍后会讨论这个问题)。

这种类型的函数是可预测的,更容易测试,因为我们总是会得到预期的输出。

React如何实现纯函数的概念

一个React应用组件的最简单形式是这样的。

`` const Counter = ({ count }) => { return <h3>{Count: ${count}`}; };

```

这类似于JavaScript中的纯函数,其中一个函数接收一个参数(在这种情况下,一个count 道具),并使用该道具来渲染输出。

然而,React的数据流是单向的,从父组件到子组件。当状态数据传递给子组件时,它就成为一个不可变的道具,不能被接收组件修改。

因此,鉴于相同的道具,这种类型的组件总是渲染相同的JSX。而且,正因为如此,我们可以在任何页面部分重复使用该组件,而不用担心不确定性。这种类型的组件是一个纯粹的功能组件。

提高应用程序的性能

React利用纯功能的概念来提高应用程序的性能。由于React的特性,每当一个组件的状态发生变化时,React都会重新渲染该组件及其子组件,即使状态变化并不直接影响子组件。

在这种情况下,React允许我们用React.memo ,以防止不必要的重新渲染,如果它收到的道具从未改变。

通过对上述纯函数进行备忘,我们只在count 道具发生变化时才重新渲染该函数。

`` const CounterComponent = React.memo(function Counter({ count }) { return <h3>{Count: ${count}`}; });

```

状态更新中的纯功能概念

React在更新状态变量时也实现了函数式的概念,特别是当一个值是基于前一个值的时候,就像在一个计数器或复选框的情况下。

看一下下面的代码。

``` import { useState } from "react"; const App = () => { const [count, setCount] = useState(0);

const handleClick = () => setCount(count + 1);

return ( // ... ); };

const Counter = ({ count }) => { // ... };

export default App;

```

在这里,为了简洁起见,我们删除了部分代码,但扩展了我们之前的Counter 组件,以显示一个按钮来增加一个计数

对于React初学者来说,这是一段熟悉的代码。虽然这段代码可以使用,但我们可以通过遵循函数式编程的概念来增加改进。

让我们专注于代码的这一部分。

``` const handleClick = () => setCount(count + 1);

```

setCount 更新器函数里面,我们使用了一个count 变量,不作为参数传递。正如我们所了解的,这违背了函数式编程的概念。

React提供的一个改进是向updater函数传递一个回调。在这个回调中,我们可以访问状态的上一个版本,从那里,我们可以更新状态值。

``` const handleClick = () => setCount((prev) => prev + 1);

```

正如我们在setCount 回调中所看到的,输出只在prev 输入参数上进行计算。这就是纯函数式概念的作用。

避免变异数据(不变性)

当一个函数突变或改变一个全局变量时,会导致我们的程序出现错误。

在函数式编程中,我们将数组和对象等可变数据结构视为不可变数据。这意味着我们从不修改它们,而是在传递给函数时做一个拷贝,这样函数就可以根据这个拷贝来计算它的输出。

让我们重新审视一下下面的代码。

``` const arr = [2, 4, 6];

function addElement(arr, ele) { return [...arr, ele]; }

console.log("modified data", addElement(arr, 8)); // Expected Output: [2, 4, 6, 8] console.log("original data", arr); // Expected Output: [2, 4, 6]

```

我们之前已经看过这段代码,但这次我们将把重点转向不可变的函数方面。

在这里,我们有一个函数,它只使用全局变量的一个副本来计算输出。我们使用ES6的传播操作符()将现有数据复制到一个新的数组中,然后添加新的元素。这样一来,我们就保持了原始数组输入数据的不可变性,正如在结果中看到的那样。

React如何处理易变的状态

由于React是一个反应式库,它必须对状态变化做出 "反应",以保持DOM的更新。很明显,状态值也必须更新。

在React中,我们不直接修改状态。相反,我们使用类组件中的setState() 方法或功能组件中的updater函数来更新状态。

看一下我们之前的代码节选。

``` const handleClick = () => setCount((prev) => prev + 1);

```

在这里,我们使用updater函数,setCount ,来更新计数。当处理像数字和字符串这样的不可改变的数据时,我们必须只将更新的值传递给updater函数,或者在下一个状态依赖于上一个状态时调用回调函数。

让我们看看另一个更新字符串值的例子。

``` import { useState } from "react";

const App = () => { const [person, setPerson] = useState("");

const handleChange = (e) => { setPerson(e.target.value); };

return ( // ... ); };

export default App;

```

在这里,为了简洁起见,我们又删除了一些代码。

上面的代码更新了一个表单的文本字段,这涉及到对不可变的字符串数据的处理。所以,我们必须通过将当前输入值传递给updater函数来更新输入字段

然而,每当我们传递像数组和对象这样的易变数据时,我们必须制作一份状态数据的副本,并根据副本计算输出。注意,我们决不能修改原始状态数据。

在下面的代码中,handleChange ,在表单中的每一个按键上都会触发更新状态变量。

``` import { useState } from "react";

const App = () => { const [person, setPerson] = useState({ fName: "", lName: "" });

const handleChange = (e) => { setPerson({ ...person, e.target.name: e.target.value }); };

return ( // ... ); };

export default App;

```

从代码中可以看出,我们正在处理一个可变的对象,因此,我们必须将状态视为不可变的。同样,我们通过使用ES6传播操作符制作一个状态的副本并更新受影响的属性来做到这一点。

``` setPerson({ ...person,

});

```

还有一个改进就是确保更新器函数setPerson ,使用一个状态变量,作为回调函数的参数传递。

``` const handleChange = (e) => { setPerson((person) => ({ ...person, e.target.name: e.target.value })); };

```

现在,如果我们不遵循这个功能概念,直接对状态进行变异,会发生什么?很明显,我们的应用程序会出现一个错误。

为了看到更清晰的画面,再次访问这个CodeSandbox,并暂时从函数中注释出…person ,像这样。

``` setPerson((person) => ({ // ...person,

}));

```

现在,通过尝试在表单字段中写一些东西,文本将相互覆盖。这是一个我们想要防止的错误,我们可以通过将状态视为不可变的数据来做到这一点。

避免副作用

功能性编程代码的目的是纯粹的。React中的纯组件可以接收一个道具作为参数,并根据输入的道具来计算输出。

但有时,该组件可以进行影响和修改其范围之外的一些状态的计算。这些计算被称为副作用。这些效应的例子包括数据的获取和手动操作DOM。

这些都是我们在应用中经常执行的任务,因此,副作用是不可避免的。

下面的片段是基于我们之前的Counter 例子。

`` const Counter = ({ count }) => { document.title =Number of click: ${count}; return <h3>{Count: ${count}`}; };

```

在代码中,我们更新了文档的标题以反映更新的计数值。这是一个副作用,因为我们修改了不属于该组件的DOM元素,从而使该组件不纯。

直接在组件主体内执行副作用是不允许的,以避免我们的应用程序中出现不一致。相反,我们必须将这种效果与渲染逻辑隔离。React为我们提供了一个名为
的Hook
[useEffect](http://blog.logrocket.com/guide-to-react-useeffect-hook/) 来管理我们的副作用

下面的代码实现了这个Hook。

`` const Counter = ({ count }) => { useEffect(() => { document.title =Number of click: ${count}`; }, [count]);

return

{Count: ${count}}

; };

```

通过将副作用放在React的 [useEffect](http://codesandbox.io/s/admiring-hoover-s52eg?file=/src/App.js) Hook中,意味着我们可以轻松地测试和维护渲染逻辑。

React中的组合

在函数式编程中,组合是一种通过组合或连锁多个较小的函数来构建复杂函数的行为。

如果我们回忆一下本文的开头,我们提到对于一个给定的函数,cf ,我们可以将它们组合成一个更复杂的函数,像这样演示。

``` z = c(f(x))

```

但现在,我们将在React的背景下看一下这个组合的概念。

与上述功能模式类似,我们可以在React中通过使用React的children 道具注入其他组件来构建一个复杂的组件。这个道具也允许一个组件渲染不同数量的内容,而不需要提前知道内容的情况。

这使我们可以灵活地决定组件内的内容,并定制内容以获得所需的输出。

实现这一概念的组件的一个很好的例子包括Hero[Sidebar](http://blog.logrocket.com/create-sidebar-menu-react/).

建立一个可重复使用的Hero 组件

假设我们想创建一个包含不同内容的Hero 组件,我们可以在我们的应用程序中的任何地方重复使用它。

我们可以像这样开始编写组件。

``` function Hero({ children }) { return

{children}
; }

```

这段代码中使用的children 道具允许我们在一个组件的开头和结尾标签之间注入内容;在我们的例子中,是一个Hero 组件。

所以,我们可以有这样的东西。

```

Home Page

This is the home page description

```

现在,在<Hero> 之间的所有内容都被认为是其children 的道具,因此出现在Hero 组件的section 标签之间。

同样地,<Banner> JSX标签内的内容作为children prop进入Banner 组件。

``` function Banner({ children }) { return (

{children}
); }

```

<Banner> 标签之间的内容(即h1p )在JSX中取代了children

在这段代码中,Banner 组件只知道button 元素,因为我们已经手动添加了该元素;它不知道什么会取代children 的道具。

这使得该组件可以重复使用,并且可以灵活地进行定制,因为我们可以控制作为children 的内容。我们现在可以决定不在我们应用程序的另一个页面上渲染横幅标题,h1

我们所要做的就是把它从内容中排除 ,在 [Banner](http://codesandbox.io/s/angry-chebyshev-69hiw?file=/src/App.js) 标签中。

通过比较React组合和数学定义,我们可以说Banner 组件的输出成为Hero 组件的输入。换句话说,HeroBanner 组件组成了一个完整的组件。

这就是实践中的组合。

总结

我很高兴你在这里!在这篇文章中,我们通过实际的例子了解到React是如何应用函数式编程的概念来做一些决定的。

我希望你喜欢阅读这篇文章。如果你有问题或贡献,请在评论区分享你的想法,我将很乐意招待他们。最后,努力在网络上分享这份指南。

The post Fundamentalsof functional programming with Reactappeared first onLogRocket Blog.