使用React的函数式编程的基础知识
理解函数式编程的概念是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))
```
在这里,我们有两个函数,c
和f
,它们被组合在一起形成一个更复杂的函数。在数学中,我们说c
是f(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
,我们可以从结果中看到。
请注意,这段代码也使用了不变性的概念来使函数变得纯粹(我们稍后会讨论这个问题)。
这种类型的函数是可预测的,更容易测试,因为我们总是会得到预期的输出。
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中的组合
在函数式编程中,组合是一种通过组合或连锁多个较小的函数来构建复杂函数的行为。
如果我们回忆一下本文的开头,我们提到对于一个给定的函数,c
和f
,我们可以将它们组合成一个更复杂的函数,像这样演示。
``` 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
道具允许我们在一个组件的开头和结尾标签之间注入内容;在我们的例子中,是一个Hero
组件。
所以,我们可以有这样的东西。
```
Home Page
This is the home page description
```
现在,在<Hero>
之间的所有内容都被认为是其children
的道具,因此出现在Hero
组件的section
标签之间。
同样地,<Banner>
JSX标签内的内容作为children
prop进入Banner
组件。
``` function Banner({ children }) { return (
```
<Banner>
标签之间的内容(即h1
和p
)在JSX中取代了children
。
在这段代码中,Banner
组件只知道button
元素,因为我们已经手动添加了该元素;它不知道什么会取代children
的道具。
这使得该组件可以重复使用,并且可以灵活地进行定制,因为我们可以控制作为children
的内容。我们现在可以决定不在我们应用程序的另一个页面上渲染横幅标题,h1
。
我们所要做的就是把它从内容中排除 ,在 [Banner](http://codesandbox.io/s/angry-chebyshev-69hiw?file=/src/App.js)
标签中。
通过比较React组合和数学定义,我们可以说Banner
组件的输出成为Hero
组件的输入。换句话说,Hero
与Banner
组件组成了一个完整的组件。
这就是实践中的组合。
总结
我很高兴你在这里!在这篇文章中,我们通过实际的例子了解到React是如何应用函数式编程的概念来做一些决定的。
我希望你喜欢阅读这篇文章。如果你有问题或贡献,请在评论区分享你的想法,我将很乐意招待他们。最后,努力在网络上分享这份指南。
The post Fundamentalsof functional programming with Reactappeared first onLogRocket Blog.
- 在C 中把字符串转换为整数的两种简单方法
- 如何在Flutter中实现任何UI
- Gatsby v4的新内容
- 创建一个Puppeteer微服务以部署到Google Cloud Functions
- 在Blazor中测试。一个完整的教程
- 在React中使用Plotly来构建动态图表
- 分页、加载更多按钮和无限滚动的指南
- 用新的Firebase v9.x Web SDK重构一个React应用
- 在使用地理定位API时,你需要知道什么?
- 在PostgreSQL v14中,JSON有什么新功能?
- 使用React的函数式编程的基础知识
- 使用Dart FFI访问Flutter中的本地库
- 使用视频播放器插件在Flutter中处理视频
- 改进过度约束的Rust库API
- 用Svelte建立一个PWA
- 用Flask和D3.js构建交互式图表
- 在Go中使用JSON。带例子的指南
- 一篇文章入门Unix中的AWK命令!
- C 哈希
- Dotfiles - 什么是Dotfile以及如何在Mac和Linux中创建它