通过一个简单的示例深入解释 React.useEffect hook

2025-06-10

通过一个简单的示例深入解释 React.useEffect hook

useEffecthook 是一种功能极其强大且用途广泛的工具,它甚至允许您创建自己的自定义钩子。

但使用它需要令人惊讶的微妙之处,所以在本文中我们将逐步了解这个钩子究竟是如何工作的。

为了不失去焦点,我们将使用可以想象到的最基本的示例,并且在每个步骤中,我们将通过将消息记录到浏览器控制台来控制正在发生的事情。

强烈建议您遵循本文并自己编写所有示例,例如使用像这样的在线 React repl 。

让我们开始吧!

基本使用和行为

useEffect顾名思义,它是一个在组件生命周期内执行任意副作用的钩子。

它基本上是“老式”生命周期方法componentDidMountcomponentDidUpdate和 的钩子替代品componentWillUnmount

它允许你无需类组件即可执行生命周期任务。因此,你现在可以在函数式组件内部产生副作用。这

以前这是不可能的,因为直接在render方法(或函数式组件的主体)中创建副作用是被严格禁止的。主要是因为我们无法真正控制(也不应该真正考虑)render函数会被调用多少次。

通过使用 可以解决这一不可预测性问题useEffect

因此,让我们创建一个简单的功能组件,我们将其称为Example

const Example = () => {
    return <div />;
};
Enter fullscreen mode Exit fullscreen mode

它实际上并没有做任何有趣的事情,因为为了解释的目的,我们希望尽可能保持简单。

请注意,我们没有使用简化的箭头语法,在简化的箭头语法中,我们可以简单地提供一个函数的返回值(在这种情况下是一个div元素)来代替函数体。这是因为我们已经知道我们将在函数体中添加一些副作用。

我们就这么做吧。

我之前提到过,禁止在组件主体中直接useEffect产生副作用。这时钩子就派上用场了:

import { useEffect } from 'react';

const Example = () => {
    useEffect(() => {
        console.log('render');
    });

    return <div />;
};
Enter fullscreen mode Exit fullscreen mode

如你所见,我们使用了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>
};
Enter fullscreen mode Exit fullscreen mode

我们创建了一个名为 的新组件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>
};
Enter fullscreen mode Exit fullscreen mode

现在我们有条件地渲染Example,只有count小于 5 时才渲染。这意味着当计数器达到 5 时,我们的组件将从视图中消失,并且 React 机制将触发其卸载阶段。

现在发现,如果你点击计数器按钮 5 次,最后一次点击时,该render字符串不会出现在控制台中。这意味着它只会在组件首次渲染时出现一次,在组件重新渲染时出现 4 次,但在第五次点击(组件从视图中消失时)不会出现。

因此我们了解到卸载组件不会触发回调。

那么如何创建与生命周期方法等效的代码呢componentWillUnmount?让我们来看看。

const Example = () => {
    useEffect(() => {
        console.log('render');
        return () => {
            console.log('unmount');
        };
    });

    return <div />;
};
Enter fullscreen mode Exit fullscreen mode

如果你对所有回调感到头晕目眩,没关系——我也是。但请注意,我们没有做任何太疯狂的事情。传递给useEffect函数的回调现在返回了另一个函数。你可以将返回的函数视为清理函数。

惊喜正在等着我们。我们原本以为这个清理函数只会在组件卸载时运行,也就是按钮上的计数器从 4 变为 5 时。

但事实并非如此。如果你在控制台中运行此示例,你会看到该字符串unmount在组件卸载时以及组件即将重新渲染时出现在控制台中。

所以最后,控制台看起来是这样的:

render
unmount
render
unmount
render
unmount
render
unmount
render
unmount
Enter fullscreen mode Exit fullscreen mode

您可以看到,每个render(当useEffect主回调执行时)都伴随着相应的unmount(当执行清理函数时)。

这两个“阶段”——效果和清理——总是成对出现。

由此可见,该模型与传统的类组件生命周期回调有所不同。它似乎更加严格,也更加固执己见。

但为什么要这样设计呢?为了找到答案,我们需要了解useEffecthook 是如何与组件 props 配合的。

useEffect 和 props

我们的Wrapper组件已经有一个状态 - count- 我们可以将其传递到Example组件中,以查看它将如何useEffect与 props 一起运行。

我们Wrapper按照如下方式修改组件:

<Example count={count} />
Enter fullscreen mode Exit fullscreen mode

然后我们更新Example组件本身:

const Example = ({ count }) => {
    // no changes here

    return <div>{count}</div>;
};
Enter fullscreen mode Exit fullscreen mode

事实证明,简单地将计数器作为道具传递,甚至将其显示在div组件的元素中,都不会以任何方式改变钩子的行为。

更重要的是,使用这个道具的useEffect行为正如我们预期的那样,同时也让我们更深入地了解useEffect主要回调和清理函数之间的关系。

此代码中,我们只需将countprop 添加到我们的日志中:

const Example = ({ count }) => {
    useEffect(() => {
        console.log(`render - ${count}`);
        return () => {
            console.log(`unmount - ${count}`);
        };
    });

    return <div>{count}</div>;
};
Enter fullscreen mode Exit fullscreen mode

当您开始单击计数器按钮时,将产生以下输出:

render - 0
unmount - 0
render - 1
unmount - 1
render - 2
unmount - 2
render - 3
unmount - 3
render - 4
unmount - 4
Enter fullscreen mode Exit fullscreen mode

这似乎是一个微不足道的结果,但它强化了我们所学到的有关主回调useEffect及其清理函数的知识——它们总是成对出现。

请注意,每个清理函数甚至使用与其各自的回调相同的道具。

例如,第一个回调的计数设置为 0,而其清理函数使用相同的值,而不是 1,这属于下一对效果和清理。

这是设计钩子的关键useEffect。你可能会问,为什么它如此重要?

例如,想象一下您的组件必须使用以下 API 建立与服务的连接:

class Service {
    subscribe(id) {},
    unsubscribe(id) {},
}
Enter fullscreen mode Exit fullscreen mode

id此服务要求您使用最初订阅时所使用的相同密码取消订阅。如果不这样做,您将留下一个 OPN 连接,这将导致数据泄露,最终甚至可能导致服务崩溃!

幸运的是,useEffect它的架构强制实施了适当的设计。

请注意,如果通过 props 向组件传递了id所需内容Service,那么您所要做的就是在该组件内写入:

useEffect(() => {
    service.subscribe(id);
    return () => {
        service.unsubscribe(id);
    };
});
Enter fullscreen mode Exit fullscreen mode

正如我们在日志示例中所看到的,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>;
};
Enter fullscreen mode Exit fullscreen mode

因为我们的useEffect函数使用了countprop,并且我们希望每次计数发生变化时将字符串记录到控制台,所以我们提供了第二个参数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>
    );
};
Enter fullscreen mode Exit fullscreen mode

您可以看到我们现在正在渲染两个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
Enter fullscreen mode Exit fullscreen mode

因此,正如您所看到的,如果您count在数组中包含第二个参数useEffect,则仅当 prop 的值发生变化时以及在组件生命周期的开始和结束时才会触发钩子。

因此,由于我们的第二个Example组件在整个过程中都传递了 -1 count,我们只看到它的两个日志 - 首次安装时和卸载时(count < 5条件开始为假之后)。

即使我们为Example组件提供一些其他道具,并且这些道具会经常变化,第二个组件仍然只会记录两次,因为它现在只监视count道具的变化。

如果您想对其他一些道具的变化做出反应,您必须将它们包含在useEffect数组中。

另一方面,在Example代码片段的第一个组件中,count每次单击按钮时,prop 的值都会增加 1,因此该组件每次都会生成日志。

现在让我们回答之前问过自己的一个问题。如何实现一个只在组件生命周期开始和结束时运行的副作用?

事实证明,您甚至可以将空数组传递给useEffect函数:

useEffect(() => {
    console.log('render');
    return () => {
        console.log('unmount');
    };
}, []);
Enter fullscreen mode Exit fullscreen mode

因为useEffect只有在挂载和卸载时才会触发回调,以及数组中的值发生变化,而数组中没有值,所以这些效果只会在组件生命的开始和结束时调用。

现在,您将在控制台中看到render组件首次渲染和unmount消失的时间。重新渲染将完全静默。

概括

可能需要消化很多内容。因此,我们来做一个简短的总结,以帮助你记住本文中最重要的概念:

  • useEffecthook 是一种在函数式组件中产生副作用的机制。副作用不应直接在组件主体或render函数中引发,而应始终将其包装在传递给 的回调函数中useEffect
  • 您可以选择在回调中返回另一个回调,用于清理操作。主回调和清理回调总是成对触发,并且使用完全相同的 props。
  • 默认情况下,useEffect回调(以及相应的清理)会在首次渲染、每次重新渲染以及卸载时运行。如果您想更改此行为,请将一个值数组作为第二个参数添加到。这样,只有在组件挂载和卸载时,或者该数组中的值发生变化时,效果才会运行。如果您只想useEffect在挂载和卸载时触发效果,只需传递一个空数组即可。

就是这样!希望本文能帮助你深入了解useEffect它的工作原理。

它看上去可能像是一个基本而简单的钩子,但现在你可以看到它背后有多么复杂和微妙。

如果您喜欢这篇文章,请在Twitter上关注我,我将在那里发布更多关于 JavaScript 编程的文章。

感谢阅读!

(封面照片由milan degraeveUnsplash上拍摄)

鏂囩珷鏉ユ簮锛�https://dev.to/mpodlasin/react-useeffect-hook-explained-in-depth-on-a-simple-example-19ec
PREV
使用 🍃 MongoDB Atlas、💙 Microsoft Azure ML/AI 和 ⚡ 无服务器函数构建自定义 WordPress Nodejs 应用!🛡 🎛 Ⓦ ⚡ 🤖 🎯
NEXT
我对 React 状态管理库无休止的争论的看法(setState/useState vs Redux vs Mobx)