清除 useEffect 副作用

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

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://www.fly63.com/article/detial/11078

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

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

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

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

react 函数子组件(Function ad Child Component)

函数子组件(FaCC )与高阶组件做的事情很相似, 都是对原来的组件进行了加强,类似装饰者。FaCC,利用了react中children可以是任何元素,包括函数的特性,那么到底是如何进行增强呢?

Vue和React组件之间的传值方式

在现代的三大框架中,其中两个Vue和React框架,组件间传值方式有哪些?组件间的传值是灵活的,可以有多种途径,父子组件同样可以使用EventBus,Vuex或者Redux

vue.js自定义组件directives

自定义指令:以v开头,如:v-mybind。bind的作用是定义一个在绑定时执行一次的初始化动作,观察bind函数,它将指令绑定的DOM作为一个参数,在函数体中,直接操作DOM节点为input赋值。

vue中prop属性传值解析

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

Web组件简介

Web组件由三个独立的技术组成:自定义元素。很简单,这些是完全有效的HTML元素,包含使用一组JavaScript API制作的自定义模板,行为和标记名称(例如,<one-dialog>)。

web组件调用其他web资源

web组件可以直接或间接的调用其他web资源。一个web组件通过内嵌返回客户端内容的另一个web资源的url来间接调用其他web资源。在执行时,一个web资源通过包含另一个资源的内容或者转发请求到另一个资源直接调用。

vue中如何实现的自定义按钮

在实际开发项目中,有时我们会用到自定义按钮;因为一个项目中,众多的页面,为了统一风格,我们会重复用到很多相同或相似的按钮,这时候,自定义按钮组件就派上了大用场,我们把定义好的按钮组件导出,在全局引用,就可以在其他组件随意使用啦,这样可以大幅度的提高我们的工作效率。

Vue子组件调用父组件的方法

Vue中子组件调用父组件的方法,这里有三种方法提供参考,第一种方法是直接在子组件中通过this.$parent.event来调用父组件的方法,第二种方法是在子组件里用$emit向父组件触发一个事件,父组件监听这个事件就行了。

点击更多...

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