了解React的useEffect清理功能 2021-11-10 默认分类 暂无评论 2898 次阅读 ![](http://www.guobacai.com/usr/uploads/2021/11/4040049004.png) React的`useEffect`清理功能通过清理效果,使应用程序免受不必要的行为,如内存泄漏。这样做,我们可以优化我们的应用程序的性能。 要开始这篇文章,你应该对什么是`useEffect`有一个基本的了解,包括用它来获取API。本文将解释`useEffect` Hook的清理功能,希望在本文结束时,你应该能够自如地使用这个清理功能。 ## 什么是`useEffect`清理功能? 就像名字所暗示的那样,`useEffect`清理是`useEffect` Hook中的一个函数,它允许我们在组件卸载前整理我们的代码。当我们的代码在每次渲染时运行和重新运行时,`useEffect`也会使用清理函数对自己进行清理。 `useEffect` Hook的构建方式是,我们可以在其中返回一个函数,这个返回函数就是清理发生的地方。清理函数可以防止内存泄漏,并删除一些不必要的和不需要的行为。 请注意,你也不会在返回函数中更新状态: ```javascript useEffect(() => { effect return () => { cleanup } }, [input]) ``` ## 为什么useEffect清理功能很有用? 如前所述,`useEffect`清理函数可以帮助开发者清理效果,防止不需要的行为,优化应用程序的性能。 然而,需要注意的是,`useEffect`清理函数不仅仅是在我们的组件想要卸载时运行,它也会在下一个预定效果执行前运行。 事实上,在我们的效果执行后,下一个预定效果通常是基于`dependency(array)`: ```javascript // The dependency is an array useEffect( callback, dependency ) ``` 因此,当我们的效果依赖于我们的道具或者任何时候我们设置了一些持久化的东西,那么我们就有理由调用清理函数。 让我们来看看这个场景:想象一下,我们通过一个用户的`id`来获取一个特定的用户,在获取完成之前,我们改变主意,试图获取另一个用户。在这一点上,道具,或者在这种情况下,`id`,在之前的获取请求仍在进行时就会更新。 这时,我们有必要使用清理函数中止获取,这样我们就不会让我们的应用程序暴露在内存泄漏中。 什么时候我们应该使用`useEffect`清理? 比方说,我们有一个获取和渲染数据的`React`组件。如果我们的组件在我们的承诺解决之前卸载,`useEffect`将尝试更新状态(在一个未卸载的组件上),并发送一个看起来像这样的错误。 ![](http://www.guobacai.com/usr/uploads/2021/11/1937041989.png) 为了解决这个错误,我们使用清理功能来解决它。 根据React的官方文档,"React会在组件卸载时执行清理工作。然而......效果在每次渲染时都会运行,而不是只有一次。这就是为什么React在下次运行特效之前也会清理前一次渲染的特效"。 清理通常用于取消所有已进行的订阅和取消获取请求。现在,让我们写一些代码,看看我们如何完成这些取消。 ## 清理一个订阅 要开始清理一个订阅,我们必须首先取消订阅,因为我们不想让我们的应用程序暴露在内存泄漏中,我们想优化我们的应用程序。 为了在我们的组件卸载之前取消订阅,让我们把我们的变量`isApiSubscribed`设置为`true`,然后当我们想卸载时,我们可以把它设置为`false`。 ```javascript useEffect(() => { // set our variable to true const isApiSubscribed = true; axios.get(API).then((response) => { if (isApiSubscribed) { // handle success } }); return () => { // cancel the subscription isApiSubscribed = false; }; }, []); ``` 在上面的代码中,我们将变量`isApiSubscribed`设置为`true`,然后将其作为一个条件来处理我们的成功请求。然而,当我们卸载我们的组件时,我们将变量`isApiSubscribed`设置为`false`。 ## 取消一个获取请求 有不同的方法来取消获取请求的调用:要么我们使用`AbortController`,要么我们使用`Axios`的取消标记。 要使用`AbortController`,我们必须使用`AbortController()`构造函数创建一个控制器。然后,当我们的获取请求开始时,我们将`AbortSignal`作为一个选项传递给请求的`option`对象。 这就把控制器和信号与获取请求联系起来,让我们随时用`AbortController.abort()`取消它: ```javascript >useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch(API, { signal: signal }) .then((response) => response.json()) .then((response) => { // handle success }); return () => { // cancel the request before component unmounts controller.abort(); }; }, []); ``` 我们可以更进一步,在我们的catch中添加一个错误条件,这样我们的fetch请求就不会在我们中止时抛出错误。这个错误的发生是因为,在卸载的时候,我们在处理错误的时候仍然试图更新状态。 我们可以做的是写一个条件,知道我们会得到什么样的错误;如果我们得到一个中止错误,那么我们就不想更新状态: ```javascript useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch(API, { signal: signal }) .then((response) => response.json()) .then((response) => { // handle success console.log(response); }) .catch((err) => { if (err.name === 'AbortError') { console.log('successfully aborted'); } else { // handle error } }); return () => { // cancel the request before component unmounts controller.abort(); }; }, []); ``` 现在,即使我们不耐烦,在我们的请求解决之前浏览到另一个页面,我们也不会再得到那个错误,因为请求会在组件解除挂载之前中止。如果我们得到一个中止错误,状态也不会更新。 所以,让我们看看如何使用Axios的取消选项,即Axios取消令牌来做同样的事情。 我们首先将Axios的`CancelToken.source()`存储在一个名为source的常量中,将该令牌作为一个Axios选项传递,然后随时用`source.cancel()`取消请求: ```javascript useEffect(() => { const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios .get(API, { cancelToken: source.token }) .catch((err) => { if (axios.isCancel(err)) { console.log('successfully aborted'); } else { // handle error } }); return () => { // cancel the request before component unmounts source.cancel(); }; }, []); ``` 就像我们在`AbortController`中的`AbortError`一样,Axios给了我们一个叫做`isCancel`的方法,让我们可以检查错误的原因,并知道如何处理我们的错误。 如果请求失败是因为Axios源中止或取消了,那么我们就不要更新状态。 ## 如何使用`useEffect`清理函数 让我们看一个例子,说明什么时候会发生上述错误,以及当它发生时如何使用清理函数。让我们首先创建两个文件。`Post`和`App`。继续写下面的代码: ```javascript // Post component import React, { useState, useEffect } from "react"; export default function Post() { const [posts, setPosts] = useState([]); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal }) .then((res) => res.json()) .then((res) => setPosts(res)) .catch((err) => setError(err)); }, []); return ( {!error ? ( posts.map((post) => ( {post.title} )) ) : ( {error} )} ); } ``` 这是一个简单的帖子组件,可以在每次渲染时获得帖子并处理获取错误。 在这里,我们在主组件中导入帖子组件,并在我们点击按钮时显示帖子。这个按钮可以显示和隐藏帖子,也就是说,它可以装载和卸载我们的帖子组件。 ```javascript // App component import React, { useState } from "react"; import Post from "./Post"; const App = () => { const [show, setShow] = useState(false); const showPost = () => { // toggles posts onclick of button setShow(!show); }; return ( Show Posts {show && } ); }; export default App; ``` 现在,点击按钮,在帖子渲染之前,再次点击按钮(在另一种情况下,它可能在帖子渲染之前导航到另一个页面),我们在控制台得到一个错误。 这是因为React的`useEffect`仍在运行,试图在后台获取API。当它获取完API后,它又试图更新状态,但这次是在一个未挂载的组件上,所以它抛出了这个错误。 ![](http://www.guobacai.com/usr/uploads/2021/11/1736698463.png) 现在,为了清除这个错误并停止内存泄漏,我们必须使用上述任何一种解决方案实现清理功能。在这篇文章中,我们将使用`AbortController`: ```javascript // Post component import React, { useState, useEffect } from "react"; export default function Post() { const [posts, setPosts] = useState([]); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal }) .then((res) => res.json()) .then((res) => setPosts(res)) .catch((err) => { setError(err); }); return () => controller.abort(); }, []); return ( {!error ? ( posts.map((post) => ( {post.title} )) ) : ( {error} )} ); } ``` 我们仍然在控制台中看到,即使在清理函数中中止了信号,解挂也会抛出一个错误。正如我们前面所讨论的,这个错误发生在我们中止fetch的调用时。 `useEffect`在catch块中捕捉到了fetch错误,然后试图更新错误状态,这时就会抛出一个错误。为了停止这种更新,我们可以使用一个`if else`条件,并检查我们得到的错误类型。 如果是一个中止错误,那么我们就不需要更新状态,否则就处理这个错误: ```javascript // Post component import React, { useState, useEffect } from "react"; export default function Post() { const [posts, setPosts] = useState([]); const [error, setError] = useState(null); useEffect(() => { const controller = new AbortController(); const signal = controller.signal; fetch("https://jsonplaceholder.typicode.com/posts", { signal: signal }) .then((res) => res.json()) .then((res) => setPosts(res)) .catch((err) => { if (err.name === "AbortError") { console.log("successfully aborted"); } else { setError(err); } }); return () => controller.abort(); }, []); return ( {!error ? ( posts.map((post) => ( {post.title} )) ) : ( {error} )} ); } ``` 注意,我们只应该在使用fetch时使用`err.name === "AbortError"`,在使用Axios时使用`axios.isCancel()`方法。 就这样,我们完成了! ## 总结 `useEffect`有两种类型的副作用:一种是不需要清理的,另一种是需要清理的,比如我们上面看到的例子。我们必须学会何时以及如何使用`useEffect Hook`的清理功能,以防止内存泄漏和优化应用程序。 我希望这篇文章对你有帮助,并能正确使用清理功能。 ## 全面了解生产中的React应用 调试React应用可能很困难,尤其是当用户遇到难以重现的问题时。如果你对监控和跟踪Redux状态感兴趣,自动浮现JavaScript错误,并跟踪缓慢的网络请求和组件加载时间,请尝试[LogRocket](http://www.guobacai.com/index.php/go/url-210-1/ "LogRocket")。 ![](http://www.guobacai.com/usr/uploads/2021/11/4242688567.png) [LogRocket](http://www.guobacai.com/index.php/go/url-210-1/ "LogRocket")就像网络应用的DVR,记录你的React应用上发生的一切。你不必猜测问题发生的原因,而是可以汇总并报告问题发生时你的应用程序处于什么状态。LogRocket还可以监控你的应用程序的性能,用客户端CPU负载、客户端内存使用等指标进行报告。 LogRocket Redux中间件软件包为你的用户会话增加了一层可见性。LogRocket记录了你的Redux商店的所有行动和状态。 使你调试React应用程序的方式现代化 - [开始免费监测](http://www.guobacai.com/index.php/go/url-210-1/http:// "开始免费监测")。 标签: 组件, 功能, 函数, 代码, useeffect, 清理, hook, 返回
评论已关闭