了解React的useEffect清理功能

1,814 阅读8分钟

React的useEffect 清理功能通过清理效果,使应用程序免于不必要的行为,如内存泄漏。这样做,我们可以优化我们应用程序的性能。

要开始这篇文章,你应该对什么是useEffect 有一个基本的了解,包括使用它来获取API。本文将解释useEffect Hook的清理功能,希望在本文结束时,你应该能够自如地使用清理功能。

什么是useEffect 清理功能?

正如其名称所暗示的,useEffect 清理是一个 [useEffect](https://blog.logrocket.com/guide-to-react-useeffect-hook/)Hook中的一个函数,它允许我们在组件卸载前整理我们的代码。当我们的代码在每次渲染时运行和重新运行时,useEffect 也会使用清理函数对自己进行清理。

useEffect Hook的构建方式是,我们可以在它里面返回一个函数,这个返回函数就是清理发生的地方。清理函数可以防止内存泄漏,并删除一些不必要的和不需要的行为。

注意,你也不会在返回函数里面更新状态。

useEffect(() => {
        effect
        return () => {
            cleanup
        }
    }, [input])

为什么useEffect 清理函数是有用的?

如前所述,useEffect 清理函数可以帮助开发者清理效果,防止不需要的行为,优化应用程序的性能。

然而,需要注意的是,useEffect 清理函数不仅仅是在我们的组件要卸载时运行,它还会在下一个预定效果执行前运行。

事实上,在我们的效果执行后,下一个预定效果通常是基于dependency(array)

// The dependency is an array
useEffect( callback, dependency )

因此,当我们的效果依赖于我们的道具或者任何时候我们设置了一些持久化的东西,那么我们就有理由调用清理函数。

让我们来看看这个场景:想象一下,我们通过一个用户的id ,得到一个特定用户的取数,在取数完成之前,我们改变主意,试图得到另一个用户。在这一点上,道具,或者在这种情况下,id ,更新了,而之前的获取请求仍在进行中。

这时,我们有必要使用清理函数中止获取,这样我们就不会让我们的应用程序暴露在内存泄漏中。

我们应该在什么时候使用useEffect cleanup?

假设我们有一个获取和渲染数据的React组件。如果我们的组件在我们的承诺解决之前解挂,useEffect 将试图更新状态(在一个未挂载的组件上),并发送一个错误,看起来像这样。

Warning Error

为了解决这个错误,我们使用cleanup函数来解决它。

根据React的官方文档,"React会在组件卸载时执行清理工作。然而......效果在每次渲染时都会运行,而不是只有一次。这就是为什么React在下次运行特效之前也会清理前一次渲染的特效"。

清理通常用于取消所有已进行的订阅和取消获取请求。现在,让我们写一些代码,看看我们如何完成这些取消。

清理一个订阅

要开始清理一个订阅,我们必须首先取消订阅,因为我们不想让我们的应用程序暴露在内存泄漏中,我们想优化我们的应用程序。

为了在我们的组件卸载之前取消订阅,让我们把我们的变量isApiSubscribed ,设置为true ,然后当我们想卸载时,我们可以把它设置为false

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](https://blog.logrocket.com/axios-or-fetch-api/)或者我们使用Axios的取消标记

要使用AbortController ,我们必须使用AbortController() 构造函数创建一个控制器。然后,当我们的获取请求开始时,我们把AbortSignal 作为一个选项传给请求的option 对象。

这就把控制器和信号与获取请求联系起来,让我们随时使用AbortController.abort() 来取消它。

>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中添加一个错误条件,这样我们的获取请求就不会在我们中止时抛出错误。这个错误的发生是因为,在卸载的时候,我们在处理错误的时候仍然试图更新状态。

我们可以做的是写一个条件,知道我们会得到什么样的错误;如果我们得到一个中止错误,那么我们就不要更新状态。

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() 取消请求。

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();
  };
}, []);

AbortError 就像我们在AbortController ,Axios给了我们一个叫做isCancel 的方法,让我们可以检查出错的原因,并知道如何处理我们的错误。

如果请求失败是因为Axios源中止或取消,那么我们就不要更新状态。

如何使用useEffect 清理功能

让我们看一个例子,说明什么时候会发生上述错误,以及当它发生时如何使用清理函数。让我们首先创建两个文件:PostApp 。继续编写以下代码。

// 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 (
    <div>
      {!error ? (
        posts.map((post) => (
          <ul key={post.id}>
            <li>{post.title}</li>
          </ul>
        ))
      ) : (
        <p>{error}</p>
      )}
    </div>
  );
}

这是一个简单的帖子组件,在每次渲染时获得帖子并处理获取错误。

在这里,我们在主组件中导入帖子组件,并在我们点击按钮时显示帖子。这个按钮可以显示和隐藏帖子,也就是说,它可以装载和卸载我们的帖子组件。

// 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 (
    <div>
      <button onClick={showPost}>Show Posts</button>
      {show && <Post />}
    </div>
  );
};
export default App;

现在,点击按钮,在帖子呈现之前,再次点击按钮(在另一种情况下,它可能在帖子呈现之前导航到另一个页面),我们在控制台得到一个错误。

这是因为React的useEffect 仍然在运行,并试图在后台获取API。当它获取完API后,它又试图更新状态,但这次是在一个未挂载的组件上,所以它抛出了这个错误。

Error Message From Updating The State Of An Unmounted Component

现在,为了清除这个错误并停止内存泄漏,我们必须使用上述任何一种解决方案来实现清理功能。在这篇文章中,我们将使用AbortController

// 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 (
    <div>
      {!error ? (
        posts.map((post) => (
          <ul key={post.id}>
            <li>{post.title}</li>
          </ul>
        ))
      ) : (
        <p>{error}</p>
      )}
    </div>
  );
}

我们仍然在控制台中看到,即使在清理函数中中止了信号,解挂也会抛出一个错误。正如我们前面所讨论的,这个错误发生在我们中止fetch的调用时。

useEffect 在catch块中抓取错误,然后试图更新错误状态,然后抛出一个错误。为了停止这种更新,我们可以使用一个if else 条件,并检查我们得到的错误类型。

如果是一个中止错误,那么我们就不需要更新状态,否则就处理这个错误。

// 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 (
    <div>
      {!error ? (
        posts.map((post) => (
          <ul key={post.id}>
            <li>{post.title}</li>
          </ul>
        ))
      ) : (
        <p>{error}</p>
      )}
    </div>
  );
}

注意,我们应该只在使用fetch时使用err.name === "AbortError"在使用Axios时使用axios.isCancel() 方法

就这样,我们完成了!

总结

useEffect 有两种类型的副作用:一种是不需要清理的,另一种是需要清理的,比如我们在上面看到的例子。我们学习何时以及如何使用useEffect Hook的清理功能来防止内存泄漏和优化应用程序是非常重要的。

我希望这篇文章对你有帮助,并且现在可以正确使用清理功能。

The postUnderstanding React's useEffect cleanup functionappeared first onLogRocket Blog.