在 useEffect 挂钩中取消所有异步/等待任务以防止反应中的内存泄漏的正确方法是啥?
Posted
技术标签:
【中文标题】在 useEffect 挂钩中取消所有异步/等待任务以防止反应中的内存泄漏的正确方法是啥?【英文标题】:What is the right way to cancel all async/await tasks within an useEffect hook to prevent memory leaks in react?在 useEffect 挂钩中取消所有异步/等待任务以防止反应中的内存泄漏的正确方法是什么? 【发布时间】:2020-02-22 00:57:05 【问题描述】:我正在开发一个从 firebase 数据库中提取数据的 react chap 应用程序。在我的“仪表板”组件中,我有一个 useEffect 挂钩来检查经过身份验证的用户,如果是,则从 firebase 中提取数据并设置电子邮件变量和聊天变量的状态。我使用 abortController 进行 useEffect 清理,但是每当我第一次注销并重新登录时,都会收到内存泄漏警告。
index.js:1375 警告:无法对未安装的组件执行 React 状态更新。这是一个空操作,但它表明您的应用程序中存在内存泄漏。要解决此问题,请在 useEffect 清理函数中取消所有订阅和异步任务。
在仪表板中(由 Context.Consumer 创建)
最初我没有 abortController,我只是在清理时返回了一个控制台日志。做了更多的研究并发现了 abortController,但是这些示例使用了 fetch 和 signal,我找不到任何与 async/await 一起使用的资源。我愿意更改数据的检索方式(无论是使用 fetch、async/await 还是任何其他解决方案)我只是无法让它与其他方法一起使用。
const [email, setEmail] = useState(null);
const [chats, setChats] = useState([]);
const signOut = () =>
firebase.auth().signOut();
;
useEffect(() =>
const abortController = new AbortController();
firebase.auth().onAuthStateChanged(async _user =>
if (!_user)
history.push('/login');
else
await firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.onSnapshot(async res =>
const chatsMap = res.docs.map(_doc => _doc.data());
console.log('res:', res.docs);
await setEmail(_user.email);
await setChats(chatsMap);
);
);
return () =>
abortController.abort();
console.log('aborting...');
;
, [history, setEmail, setChats]);
预期结果是在 useEffect 清理函数中正确清理/取消所有异步任务。一个用户注销后,相同或不同的用户重新登录后,我在控制台中收到以下警告
index.js:1375 警告:无法对未安装的组件执行 React 状态更新。这是一个空操作,但它表明您的应用程序中存在内存泄漏。要修复,请取消所有订阅 和 useEffect 清理函数中的异步任务。
在仪表板中(由 Context.Consumer 创建)
【问题讨论】:
您可以使用AbortController
停止fetch
。我认为它不适用于正常承诺等其他事情。
您介意展示如何将 fetch 与上述 useEffect 结合起来(以便我可以测试解决方案)吗?在这种情况下,我找不到有关使用 fetch 从 firebase 数据库中提取信息的文档。
【参考方案1】:
对于 firebase,您处理的不是async/await
,而是流。您应该在清理功能中取消订阅 firebase 流:
const [email, setEmail] = useState(null);
const [chats, setChats] = useState([]);
const signOut = () =>
firebase.auth().signOut();
;
useEffect(() =>
let unsubscribeSnapshot;
const unsubscribeAuth = firebase.auth().onAuthStateChanged(_user =>
// you're not dealing with promises but streams so async/await is not needed here
if (!_user)
history.push('/login');
else
unsubscribeSnapshot = firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.onSnapshot(res =>
const chatsMap = res.docs.map(_doc => _doc.data());
console.log('res:', res.docs);
setEmail(_user.email);
setChats(chatsMap);
);
);
return () =>
unsubscribeAuth();
unsubscribeSnapshot && unsubscribeSnapshot();
;
, [history]); // setters are stable between renders so you don't have to put them here
【讨论】:
有意思,谢谢!我是firebase的新手,所以不深入了解。你能解释一下“unsubscribeSnapshot && unsubscribeSnapshot();”吗?为什么不只使用“unsubscribeSnapshot();”在回报? @danielunsubscribeSnapshot
可以是 undefined
,因为它仅在 _user
为真时设置。如果你打电话给undefined
,你的应用就会崩溃。【参考方案2】:
onSnapshot
method 确实不返回一个承诺,所以等待它的结果是没有意义的。相反,它开始 侦听数据(并更改该数据),并使用相关数据调用onSnapshot
回调。这可能会发生多次,因此它不能返回承诺。侦听器一直连接到数据库,直到您通过调用从onSnapshot
返回的方法取消订阅它。由于您从不存储该方法,更不用说调用它,监听器保持活动状态,稍后将再次调用您的回调。这很可能是内存泄漏的来源。
如果您想等待 Firestore 的结果,您可能正在寻找 get()
method。这会获取一次数据,然后解析 Promise。
await firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.get(async res =>
【讨论】:
实施您的建议后,我在注销/登录时仍然收到相同的“内存泄漏”警告。``` //减少显示 if (!_user) history.push('/login '); else const userChatSnapshot = await firebase .firestore() .collection('chats') .where('users', 'array-contains', _user.email) .get();常量 chatsMap = userChatSnapshot.docs.map(_doc => _doc.data());等待 setChats(chatsMap);等待 setEmail(_user.email); ); ,[历史,setEmail,setChats]); ```【参考方案3】:取消async/await
的一种方法是创建类似于内置AbortController
的东西,它将返回两个函数:一个用于取消,一个用于检查取消,然后在async/await
中的每个步骤之前检查取消需要运行:
function $AbortController()
let res, rej;
const p = new Promise((resolve, reject) =>
res = resolve;
rej = () => reject($AbortController.cSymbol);
)
function isCanceled()
return Promise.race([p, Promise.resolve()]);
return [
rej,
isCanceled
];
$AbortController.cSymbol = Symbol("cancel");
function delay(t)
return new Promise((res) =>
setTimeout(res, t);
)
let cancel, isCanceled;
document.getElementById("start-logging").addEventListener("click", async (e) =>
try
cancel && cancel();
[cancel, isCanceled] = $AbortController();
const lisCanceled = isCanceled;
while(true)
await lisCanceled(); // check for cancellation
document.getElementById("container").insertAdjacenthtml("beforeend", `<p>$Date.now()</p>`);
await delay(2000);
catch (e)
if(e === $AbortController.cSymbol)
console.log("cancelled");
)
document.getElementById("cancel-logging").addEventListener("click", () => cancel())
<button id="start-logging">start logging</button>
<button id="cancel-logging">cancel logging</button>
<div id="container"></div>
【讨论】:
以上是关于在 useEffect 挂钩中取消所有异步/等待任务以防止反应中的内存泄漏的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章
如何取消 useEffect 清理函数中的所有订阅和异步任务?