通过一个简单的示例深入解释 React.useEffect hook
useEffect
hook 是一种功能极其强大且用途广泛的工具,它甚至允许您创建自己的自定义钩子。
但使用它需要令人惊讶的微妙之处,所以在本文中我们将逐步了解这个钩子究竟是如何工作的。
为了不失去焦点,我们将使用可以想象到的最基本的示例,并且在每个步骤中,我们将通过将消息记录到浏览器控制台来控制正在发生的事情。
强烈建议您遵循本文并自己编写所有示例,例如使用像这样的在线 React repl 。
让我们开始吧!
基本使用和行为
useEffect
顾名思义,它是一个在组件生命周期内执行任意副作用的钩子。
它基本上是“老式”生命周期方法componentDidMount
、componentDidUpdate
和 的钩子替代品componentWillUnmount
。
它允许你无需类组件即可执行生命周期任务。因此,你现在可以在函数式组件内部产生副作用。这
以前这是不可能的,因为直接在render
方法(或函数式组件的主体)中创建副作用是被严格禁止的。主要是因为我们无法真正控制(也不应该真正考虑)render
函数会被调用多少次。
通过使用 可以解决这一不可预测性问题useEffect
。
因此,让我们创建一个简单的功能组件,我们将其称为Example
:
const Example = () => {
return <div />;
};
它实际上并没有做任何有趣的事情,因为为了解释的目的,我们希望尽可能保持简单。
请注意,我们没有使用简化的箭头语法,在简化的箭头语法中,我们可以简单地提供一个函数的返回值(在这种情况下是一个div
元素)来代替函数体。这是因为我们已经知道我们将在函数体中添加一些副作用。
我们就这么做吧。
我之前提到过,禁止在组件主体中直接useEffect
产生副作用。这时钩子就派上用场了:
import { useEffect } from 'react';
const Example = () => {
useEffect(() => {
console.log('render');
});
return <div />;
};
如你所见,我们使用了useEffect
函数,它接受一个回调函数作为参数。在回调函数内部,我们创建了一个简单的console.log
,它将帮助我们找出此效果何时执行。
如果您渲染该组件并查看浏览器控制台,您将看到render
那里记录了一次。
好的。所以我们知道,当组件首次创建和渲染时,回调肯定会被调用。但就这些吗?
为了找到答案,我们需要制作一个更复杂的例子,以便我们能够根据Example
命令重新渲染组件:
import { useState } from 'react';
const Wrapper = () => {
const [count, setCount] = useState(0);
const updateCount = () => setCount(count + 1);
return (
<div>
<button onClick={updateCount}>{count}</button>
<Example />
</div>
};
我们创建了一个名为 的新组件Wrapper
。它同时渲染了我们之前的组件Example
和一个按钮。按钮显示一个计数器值,初始值为0
。点击按钮后,计数器加一。
但计数器本身其实并不吸引我们。我们只是用它作为一个技巧来触发Example
组件的重新渲染。每当你点击计数器按钮时,Wrapper
组件的状态就会更新。这会导致 的重新渲染Wrapper
,进而导致Example
组件的重新渲染。
Example
因此基本上每次单击按钮都会导致重新渲染。
现在让我们点击按钮几次,看看控制台中发生了什么。
事实证明,每次点击后,该render
字符串都会再次出现在控制台中。因此,如果您点击按钮四次,您将render
在控制台中看到 5 个字符串:一个来自初始渲染,一个来自点击按钮引起的重新渲染。
好的,这意味着useEffect
在初始渲染和组件每次重新渲染时都会调用回调。
当组件卸载并从视图中消失时,它也会被调用吗?为了检查这一点,我们需要Wrapper
再次修改组件:
const Wrapper = () => {
// everything here stays the same as before
return (
<div>
<button onClick={updateCount}>{count}</button>
{count < 5 && <Example />}
</div>
};
现在我们有条件地渲染Example
,只有count
小于 5 时才渲染。这意味着当计数器达到 5 时,我们的组件将从视图中消失,并且 React 机制将触发其卸载阶段。
现在发现,如果你点击计数器按钮 5 次,最后一次点击时,该render
字符串不会出现在控制台中。这意味着它只会在组件首次渲染时出现一次,在组件重新渲染时出现 4 次,但在第五次点击(组件从视图中消失时)不会出现。
因此我们了解到卸载组件不会触发回调。
那么如何创建与生命周期方法等效的代码呢componentWillUnmount
?让我们来看看。
const Example = () => {
useEffect(() => {
console.log('render');
return () => {
console.log('unmount');
};
});
return <div />;
};
如果你对所有回调感到头晕目眩,没关系——我也是。但请注意,我们没有做任何太疯狂的事情。传递给useEffect
函数的回调现在返回了另一个函数。你可以将返回的函数视为清理函数。
惊喜正在等着我们。我们原本以为这个清理函数只会在组件卸载时运行,也就是按钮上的计数器从 4 变为 5 时。
但事实并非如此。如果你在控制台中运行此示例,你会看到该字符串unmount
在组件卸载时以及组件即将重新渲染时出现在控制台中。
所以最后,控制台看起来是这样的:
render
unmount
render
unmount
render
unmount
render
unmount
render
unmount
您可以看到,每个render
(当useEffect
主回调执行时)都伴随着相应的unmount
(当执行清理函数时)。
这两个“阶段”——效果和清理——总是成对出现。
由此可见,该模型与传统的类组件生命周期回调有所不同。它似乎更加严格,也更加固执己见。
但为什么要这样设计呢?为了找到答案,我们需要了解useEffect
hook 是如何与组件 props 配合的。
useEffect 和 props
我们的Wrapper
组件已经有一个状态 - count
- 我们可以将其传递到Example
组件中,以查看它将如何useEffect
与 props 一起运行。
我们Wrapper
按照如下方式修改组件:
<Example count={count} />
然后我们更新Example
组件本身:
const Example = ({ count }) => {
// no changes here
return <div>{count}</div>;
};
事实证明,简单地将计数器作为道具传递,甚至将其显示在div
组件的元素中,都不会以任何方式改变钩子的行为。
更重要的是,使用这个道具的useEffect
行为正如我们预期的那样,同时也让我们更深入地了解useEffect
主要回调和清理函数之间的关系。
此代码中,我们只需将count
prop 添加到我们的日志中:
const Example = ({ count }) => {
useEffect(() => {
console.log(`render - ${count}`);
return () => {
console.log(`unmount - ${count}`);
};
});
return <div>{count}</div>;
};
当您开始单击计数器按钮时,将产生以下输出:
render - 0
unmount - 0
render - 1
unmount - 1
render - 2
unmount - 2
render - 3
unmount - 3
render - 4
unmount - 4
这似乎是一个微不足道的结果,但它强化了我们所学到的有关主回调useEffect
及其清理函数的知识——它们总是成对出现。
请注意,每个清理函数甚至使用与其各自的回调相同的道具。
例如,第一个回调的计数设置为 0,而其清理函数使用相同的值,而不是 1,这属于下一对效果和清理。
这是设计钩子的关键useEffect
。你可能会问,为什么它如此重要?
例如,想象一下您的组件必须使用以下 API 建立与服务的连接:
class Service {
subscribe(id) {},
unsubscribe(id) {},
}
id
此服务要求您使用最初订阅时所使用的相同密码取消订阅。如果不这样做,您将留下一个 OPN 连接,这将导致数据泄露,最终甚至可能导致服务崩溃!
幸运的是,useEffect
它的架构强制实施了适当的设计。
请注意,如果通过 props 向组件传递了id
所需内容Service
,那么您所要做的就是在该组件内写入:
useEffect(() => {
service.subscribe(id);
return () => {
service.unsubscribe(id);
};
});
正如我们在日志示例中所看到的,useEffect
将确保每个subscribe
后面始终跟着,并且传递给它的值unsubscribe
完全相同。id
无论组件更新频率如何,也无论其道具如何疯狂变化,这种架构都可以使编写合理且安全的代码变得非常简单。
控制更新
对于习惯了类组件生命周期方法的人来说,useEffect
一开始往往显得有限制。
如何仅在第一次渲染时添加效果?
如何仅在组件生命结束时运行清理功能,而不是在每次重新渲染后运行?
为了找到这些问题的答案,我们需要描述useEffect
提供给我们的最后一种机制。
第二个参数useEffect
可选地接受一个值数组。这些值将与之前的值进行比较,以决定是否运行该效果。
它的工作原理有点像shouldComponentUpdate
副作用。如果值发生变化,效果就会执行。如果所有值都没有变化,则什么也不会发生。
Example
因此我们可以像这样编辑我们的组件:
const Example = ({ count }) => {
useEffect(() => {
// everything here stays the same as before
}, [count]);
return <div>{count}</div>;
};
因为我们的useEffect
函数使用了count
prop,并且我们希望每次计数发生变化时将字符串记录到控制台,所以我们提供了第二个参数useEffect
- 一个只有一个值的数组,即我们想要观察变化的 prop。
如果在重新渲染之间值count
没有改变,则不会运行效果,并且控制台中也不会出现任何日志。
为了看看真的发生了什么,我们可以编辑我们的Wrapper
组件:
const Wrapper = () => {
// everything here stays the same as before
return (
<div>
<button onClick={updateCount}>{count}</button>
{count < 5 && <Example count={count} />}
{count < 5 && <Example count={-1} />}
</div>
);
};
您可以看到我们现在正在渲染两个Example
组件。一个组件——像以前一样——通过 prop 传递count
值,而另一个组件始终获得相同的值 -1。
这样,当我们反复点击计数器按钮时,就能比较控制台输出的差异。只需记住将[count]
数组作为第二个参数即可useEffect
。
点击计数器几次后,我们得到:
render - 0
render - -1 // this was logged by the second component
unmount - 0
render - 1
unmount - 1
render - 2
unmount - 2
render - 3
unmount - 3
render - 4
unmount - 4
unmount - -1 // this was logged by the second component
因此,正如您所看到的,如果您count
在数组中包含第二个参数useEffect
,则仅当 prop 的值发生变化时以及在组件生命周期的开始和结束时才会触发钩子。
因此,由于我们的第二个Example
组件在整个过程中都传递了 -1 count
,我们只看到它的两个日志 - 首次安装时和卸载时(count < 5
条件开始为假之后)。
即使我们为Example
组件提供一些其他道具,并且这些道具会经常变化,第二个组件仍然只会记录两次,因为它现在只监视count
道具的变化。
如果您想对其他一些道具的变化做出反应,您必须将它们包含在useEffect
数组中。
另一方面,在Example
代码片段的第一个组件中,count
每次单击按钮时,prop 的值都会增加 1,因此该组件每次都会生成日志。
现在让我们回答之前问过自己的一个问题。如何实现一个只在组件生命周期开始和结束时运行的副作用?
事实证明,您甚至可以将空数组传递给useEffect
函数:
useEffect(() => {
console.log('render');
return () => {
console.log('unmount');
};
}, []);
因为useEffect
只有在挂载和卸载时才会触发回调,以及数组中的值发生变化,而数组中没有值,所以这些效果只会在组件生命的开始和结束时调用。
现在,您将在控制台中看到render
组件首次渲染和unmount
消失的时间。重新渲染将完全静默。
概括
可能需要消化很多内容。因此,我们来做一个简短的总结,以帮助你记住本文中最重要的概念:
useEffect
hook 是一种在函数式组件中产生副作用的机制。副作用不应直接在组件主体或render
函数中引发,而应始终将其包装在传递给 的回调函数中useEffect
。- 您可以选择在回调中返回另一个回调,用于清理操作。主回调和清理回调总是成对触发,并且使用完全相同的 props。
- 默认情况下,
useEffect
回调(以及相应的清理)会在首次渲染、每次重新渲染以及卸载时运行。如果您想更改此行为,请将一个值数组作为第二个参数添加到。这样,只有在组件挂载和卸载时,或者该数组中的值发生变化时,效果才会运行。如果您只想useEffect
在挂载和卸载时触发效果,只需传递一个空数组即可。
就是这样!希望本文能帮助你深入了解useEffect
它的工作原理。
它看上去可能像是一个基本而简单的钩子,但现在你可以看到它背后有多么复杂和微妙。
如果您喜欢这篇文章,请在Twitter上关注我,我将在那里发布更多关于 JavaScript 编程的文章。
感谢阅读!
(封面照片由milan degraeve在Unsplash上拍摄)
鏂囩珷鏉ユ簮锛�https://dev.to/mpodlasin/react-useeffect-hook-explained-in-depth-on-a-simple-example-19ec