正确理解并运用useEffect
首先
为了了解这篇文章的背景
我第一次接触到编程并主动去学习的是在使用React时。当时我只是专注于创建能够正常运行的东西,所以勉强能够使用useEffect,但并没有真正理解它,就一直这样进行下来。为了不在工作中吃苦头,我决定重新系统地学习一遍。
为了什么
-
- useEffect をいまだになんとなくで使っているので理解を深めたい
- useEffect を使いこなしてバグのないコードを書けるようになりたい
我不明白的事情 (wǒ bù de
-
- そもそも何をしているのか?
-
- 副作用
-
- ライフサイクル
-
- useEffect はいつ実行される?
-
- クリーンアップ
-
- dependencies(第二引数)に何を入れればいかわからない
eslint-plugin-react-hooksに脳死でしたがっていた
让我们来解答这些问题吧!
首先你在做什么呢?
首先需要参考的是React的官方文档。
在函数组件内可以执行副作用
关于副作用的详细内容将在后面提及,但通过使用useEffect,您可以执行组件渲染后的各种类型的副作用。换句话说,使用useEffect可以告诉React在渲染后需要执行某些处理。
函数组件是一种用JavaScript函数语法来表示React组件的方法。另一种方法是类组件,但我只用函数组件编写过。
useEffect是一种可以在函数组件中使用的React钩子之一。
在类组件中,对于每个组件只能写一个副作用代码,比如componentDidMount,componentDidUpdate,无法分离关注点。而在函数组件中,可以写多个useEffect,因此更容易分离关注点。
详细地说,我们基本上是按以下步骤进行处理。
-
- 使用 useState 的 set 函数等来修改函数的值
-
- 修改 DOM
-
- 重新渲染屏幕
- 在渲染后执行 useEffect 的第一个参数中传递的处理函数
補足
useEffect(() => {
// 何らかの処理
} ,[dependencies])"
における
"() => {
// 何らかの処理
}"
の箇所が第一引数のコールバック関数です。
不良反应
在编程中,“副作用”是指某个功能在程序上改变数据,从而影响到后续运算的结果。换句话说,数学函数原本的目的是返回计算结果,但在这个过程中会改变周围的状态。
它与“吃了药会感到困倦”这样的副作用有着不同的意思。
并不一定带来不良影响,重要的是将其放置在适当的位置。
我来举例说明一下没有副作用的函数和有副作用的函数。
function double(x: number) :number {
return 2 * x;
}
let num = 0;
// グローバル変数numを参照し、それを増加させて返す関数
function add(x: number): void {
num = num + x;
}
具有无副作用的函数具有引用透明性。
根据透明度参考:
– 在相同条件下,结果将是相同的
– 它不会对任何其他功能的结果产生影响
只要是有副作用的函数,就没有参照透明性,会改变函数外部的状态。上面的add函数每次执行都会产生不同的结果,所以可以说它有副作用。
这是关于编程中的副作用。我将提到前端方面的具体话题。
在 React 中,副作用指的是通过某些操作结果,导致 DOM 被修改并触发重新渲染的情况。
以下是一个简单的例子:当按下按钮时,计数的值会发生变化,并且屏幕上显示的数字也会改变的代码。
const Counter: React.FC = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((prevCount) => prevCount +1);
};
return (
<div>
<p>
カウントは{count}です。
</p>
<button onClick={handleClick}>+</button>
</div>
);
}
通过按下按钮,handleClick函数将被执行,count的值将改变,DOM将被重新编写并重新渲染。
handleClick是一个有副作用的函数。
响应式编程
React 最初是根据传递给组件的参数来操作虚拟 DOM,并通过传递的处理器来更新状态的想法而创建的,正因为这正是响应式编程,所以才被称为 React。
它是一个结构,可以通过某个组件的参数或事件来改变其他状态。
生命周期
在React中,生命周期可以分为以下三个阶段。
-
- Mounting
コンポーネントが生成されて、レンダリングされるまでの期間
-
- Updating
コンポーネントが管理するデータがユーザーによって更新される期間
-
- Unmounting
コンポーネントが不要となり、破棄するための期間
这种挂载处理和卸载处理的循环被称为生命周期。React组件在每个时机被渲染。
useEffect会在什么时候被执行?
useEffect 的执行时机取决于其第二个参数的指定。
第二引数に何も指定しない場合
レンダリング(Mounting, Updating, Unmounting)毎に実行します。
つまり、コンポーネント内で値が変わり、再レンダリングされるような場合には再レンダリング毎に実行します。
第二引数に空の配列を指定した場合
初回レンダリング時のみ実行されます。
第二引数の配列に 1 つ以上の値が指定されている場合
渡された値がレンダリング前後で変更がなければ処理をスキップし、再レンダリング後に変更していれば実行します。
我来看一下代码。
const Counter: React.FC = () => {
const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const countUp = () => {
setCount((prevCount) => prevCount + 1);
};
// useEffect_1
useEffect(() => {
console.log("再レンダリングされるたび実行");
});
// useEffect_2
useEffect(() => {
console.log("初回レンダリング時のみ実行");
},[])
// useEffect_3
useEffect(() => {
console.log("countの値が変わるたび実行");
}, [count]);
return (
<>
{console.log("----レンダリング----")}
<button onClick={countUp}>+</button>
<p>count: {count}</p>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? "close" : "open"}
</button>
</>
);
};

当改变count的值时。
-
- 通过点击「+按钮」来调用countUp函数
-
- count的值会改变
-
- 重新渲染
-
- 重新渲染后会调用useEffect_1
- 由于渲染前和渲染后count的值不同,所以会调用useEffect_3
当改变isOpen的值时。
-
- 通过点击打开或关闭按钮来调用handleToggle函数
-
- isOpen的值将发生变化
-
- 重新渲染
-
- 重新渲染后调用useEffect_1
- 由于渲染后和重新渲染后count的值是相同的,所以跳过useEffect_3
清理
如果想要设置对某些外部数据源的订阅,或者使用诸如setTimeout函数等计时器函数的话,就需要进行清理以防止内存泄漏发生。
在 `useEffect` 中,通过在副作用函数内返回清除函数,可以在组件挂载时执行操作,并且能够在后续的重新渲染时清除上一次的副作用。
让我们通过查看示例代码来理解为什么需要进行清理操作。
const Timer: React.FC<{
setIsDisplay: (value: React.SetStateAction<boolean>) => void;
}> = ({ setIsDisplay }) => {
const [count, setCount] = useState(10);
useEffect(() => {
console.log("再レンダー");
if (count < 0) {
setIsDisplay(false);
return;
}
setInterval(() => {
setCount((prev) => prev - 1);
}, 1000);
}, [count, setIsDisplay]);
return (
<p>{count}秒後にunMountします</p>
);
};
应用程序组件可控制计时器组件的显示/隐藏。
const App: React.FC = () => {
const [isDisplay, setIsDisplay] = useState(true);
const handleToggleDisplay = () => {
setIsDisplay(!isDisplay);
};
return (
<div>
<span>
コンポーネントを
<button onClick={handleToggleDisplay}>
{isDisplay ? "Unmount" : "Mount"}
</button>
</span>
{isDisplay && <Timer setIsDisplay={setIsDisplay} />}
</div>
);
}
在浏览器中运行此代码时,点击“Mount”按钮后,Timer组件将被挂载,并在渲染后执行useEffect函数中的处理代码。
在这段代码中,每当count的值发生改变时,useEffect函数都会被执行。也就是说,setTimeout函数会被同时调用,从而导致类似屏幕截图中每秒递减的count数出现不同的错误。

此外,您可以在浏览器的开发者控制台中确认到存在内存泄漏的情况。

让我们写出不会产生这些错误的代码吧。
const Timer: React.FC<{
setIsDisplay: (value: React.SetStateAction<boolean>) => void;
}> = ({ setIsDisplay }) => {
const [count, setCount] = useState(10);
useEffect(() => {
console.log("再レンダー");
if (count < 0) {
setIsDisplay(false);
return;
}
const doInterval = setInterval(() => {
setCount((prev) => prev - 1);
}, 1000);
return () => {
console.log("前回のintervalをクリーンアップします");
clearInterval(doInterval);
};
}, [count, setIsDisplay]);
return (
<p>{count}秒後にunMountします</p>
);
};
在这个变更中,我们在useEffect内添加了清理函数。
在清理函数中,在重新渲染时,我们会丢弃上一个 setInterval 函数,并在重新渲染后执行新的 setInterval 函数。这样,count 每秒都会减少1,并且还能避免内存泄漏。

依赖关系
如果在 useEffect 的第二个参数中没有指定正确的值,将会引发错误。其中一个常见的错误是无限渲染。那么,什么情况下会发生无限渲染呢?就是在 useEffect 内部的处理中将需要更改的值指定为第二个参数。
useEffect(() => {
console.log("countを更新", count);
setCount((prev) => prev + 1);
}, [count]);
為什麼會出現無限渲染的情況呢?原因是下列的2至4步驟會不斷重複。
-
- 在初始渲染时,调用 useEffect 来改变 count 的值。
-
- 由于 count 的值在渲染之前和之后发生了变化,因此会调用 useEffect。
-
- 调用 useEffect 来改变 count 的值。
- 进行重新渲染。
在useEffect内部,应该避免将需要改变的值包含在第二个参数中。
在文章开头,我提到了对eslint-plugin-react-hooks的使用过于机械而不好,但是这个规则被React官方文档推荐,并且我们必须使用它。虽然这不仅仅适用于这个特定的规则,但重要的是在了解原理的基础上使用它。
最后
非常抱歉让您阅读了这么长的文本,感谢您一直读到最后。如果有任何错误或补充,请留下评论!