清除 useEffect 副作用

更新日期: 2022-01-28 阅读: 1.9k 标签: 组件

react 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    const id = setInterval(async () => {
      const data = await fetchData();
      setList(list => list.concat(data));
    }, 2000);
    return () => clearInterval(id);
  }, [fetchData]);

  return list;
}


问题

该方法的问题在于没有考虑到 fetchData() 方法的执行时间,如果它的执行时间超过 2s 的话,那就会造成轮询任务的堆积。而且后续也有需求把这个定时时间动态化,由服务端下发间隔时间,降低服务端压力。

所以这里我们可以考虑使用 setTimeout 来替换 setInterval。由于每次都是上一次请求完成之后再设置延迟时间,确保了他们不会堆积。以下是修改后的代码

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    let id;
    async function getList() {
      const data = await fetchData();
      setList(list => list.concat(data));
      id = setTimeout(getList, 2000);
    }
    getList();
    return () => clearTimeout(id);
  }, [fetchData]);

  return list;
}

不过改成 setTimeout 之后会引来新的问题。由于下一次的 setTimeout 执行需要等待 fetchData() 完成之后才会执行。如果在 fetchData() 还没有结束的时候我们就卸载组件的话,此时 clearTimeout() 只能无意义的清除当前执行时的回调,fetchData() 后调用 getList() 创建的新的延迟回调还是会继续执行。

在线示例:CodeSandbox

可以看到在点击按钮隐藏组件之后,接口请求次数还是在继续增加着。那么要如何解决这个问题?以下提供了几种解决方案。


如何解决

Promise Effect

该问题的原因是 Promise 执行过程中,无法取消后续还没有定义的 setTimeout() 导致的。所以最开始想到的就是我们不应该直接对 timeoutID 进行记录,而是应该向上记录整个逻辑的 Promise 对象。当 Promise 执行完成之后我们再清除 timeout,保证我们每次都能确切的清除掉任务。

在线示例:CodeSandbox
import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    let getListPromise;
    async function getList() {
      const data = await fetchData();
      setList((list) => list.concat(data));
      return setTimeout(() => {
        getListPromise = getList();
      }, 2000);
    }

    getListPromise = getList();
    return () => {
      getListPromise.then((id) => clearTimeout(id));
    };
  }, [fetchData]);
  return list;
}

AbortController

上面的方案能比较好的解决问题,但是在组件卸载的时候 Promise 任务还在执行,会造成资源的浪费。其实我们换个思路想一下,Promise 异步请求对于组件来说应该也是副作用,也是需要”清除“的。只要清除了 Promise 任务,后续的流程自然不会执行,就不会有这个问题了。

清除 Promise 目前可以利用 AbortController 来实现,我们通过在卸载回调中执行 controller.abort() 方法,最终让代码走到 Reject 逻辑中,阻止了后续的代码执行。

在线示例:CodeSandbox
import { useState, useEffect } from 'react';

function fetchDataWithAbort({ fetchData, signal }) {
  if (signal.aborted) {
    return Promise.reject("aborted");
  }
  return new Promise((resolve, reject) => {
    fetchData().then(resolve, reject);
    signal.addEventListener("aborted", () => {
      reject("aborted");
    });
  });
}
function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    let id;
    const controller = new AbortController();
    async function getList() {
      try {
        const data = await fetchDataWithAbort({ fetchData, signal: controller.signal });
        setList(list => list.concat(data));
        id = setTimeout(getList, 2000);
      } catch(e) {
        console.error(e);
      }
    }
    getList();
    return () => {
      clearTimeout(id);
      controller.abort();
    };
  }, [fetchData]);

  return list;
}

状态标记

上面一种方案,我们的本质是让异步请求抛错,中断了后续代码的执行。那是不是我设置一个标记变量,标记是非卸载状态才执行后续的逻辑也可以呢?所以该方案应运而生。

定义了一个 unmounted 变量,如果在卸载回调中标记其为 true。在异步任务后判断如果 unmounted === true 的话就不走后续的逻辑来实现类似的效果。

在线示例:CodeSandbox
import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    let id;
    let unmounted;
    async function getList() {
      const data = await fetchData();
      if(unmounted) {
        return;
      }

      setList(list => list.concat(data));
      id = setTimeout(getList, 2000);
    }
    getList();
    return () => {
      unmounted = true;
      clearTimeout(id);
    }
  }, [fetchData]);

  return list;
}


后记

问题的本质是一个长时间的异步任务在过程中的时候组件卸载后如何清除后续的副作用。

这个其实不仅仅局限在本文的 Case 中,我们大家平常经常写的在 useEffect 中请求接口,返回后更新 State 的逻辑也会存在类似的问题。

只是由于在一个已卸载组件中 setState 并没有什么效果,在用户层面无感知。而且 React 会帮助我们识别该场景,如果已卸载组件再做 setState 操作的话,会有 Warning 提示。


再加上一般异步请求都比较快,所以大家也不会注意到这个问题。

所以大家还有什么其他的解决方法解决这个问题吗?欢迎评论留言~

注: 题图来自《How To Call Web APIs with the useEffect Hook in React

来自:https://segmentfault.com/a/1190000041351344

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/11078

相关推荐

vue重新渲染组件(重置或者更新)

当数据通过异步操作后,对之前加载的数据进行变更后,发现数据不生效。A组件或者B组件触发数据更新,C组件数据更新了,但是C组件仍显示上一次数据。

Vuetify基于vue2.0,为移动而生的组件框架

Vuetify 支持SSR(服务端渲染),SPA(单页应用程序),PWA(渐进式Web应用程序)和标准HTML页面。 Vuetify是一个渐进式的框架,试图推动前端开发发展到一个新的水平。

React高阶组件中使用React.forwardRef的技巧

之前使用React.forwardRef始终无法应用于React高阶组件中,关键点就是React.forwardRef的API中ref必须指向dom元素而不是React组件。codepen实例请划到底部。

Vue使用Props绑定Object并且传参

通过Props 给子组件传变量,变量是对象时,子组件无法在首次打开时获取到传入对象数据,并且在父组件中改变对象的属性,子组件也是无法监听

Vue中插槽的作用_Vue组件插槽的使用以及调用组件内的方法

通过给组件传递参数, 可以让组件变得更加可扩展, 组件内使用props接收参数,slot的使用就像它的名字一样, 在组件内定义一块空间。在组件外, 我们可以往插槽里填入任何元素。slot-scope的作用就是把组件内的数据带出来

React Hook父组件获取子组件的数据/函数

我们知道在react中,常用props实现子组件数据到父组件的传递,但是父组件调用子组件的功能却不常用。文档上说ref其实不是最佳的选择,但是想着偷懒不学redux,在网上找了很多教程,要不就是hook的讲的太少

使用Vue 自定义文件选择器组件

文件选择元素是web上最难看的 input 类型之一。它们在每个浏览器中实现的方式不同,而且通常非常难看。这里有一个解决办法,就是把它封装成一个组件。

element-ui 的隐藏滚动组件el-scrollbar

为什么要用el-scrollbar,大家都知道,模拟一个滚动不难,而且市面上有很多这样的库。我考虑的,首先项目用的框架是Vue,然后用的组件库是Element,Element官网也有很多滚动

vue中prop属性传值解析

prop的定义:在没有状态管理机制的时候,prop属性是组件之间主要的通信方式,prop属性其实是一个对象,在这个对象里可以定义一些数据,而这些数据可以通过父组件传递给子组件。 prop属性中可以定义属性的类型,也可以定义属性的初始值。

写一个vue组件库_跟着element学习写组件

组件以插件的形式引入使用,当然,也可以直接在页面引入组件文件,两者按需使用。通过源码可知,vue不会重复安装同一个插件。以第一次安装为准,现在,可以在代码中使用组件啦~

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!