共计 3482 个字符,预计需要花费 9 分钟才能阅读完成。
在 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》