在 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();”在回报? @daniel unsubscribeSnapshot 可以是 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 挂钩中进行的异步调用的测试结果

如何取消 useEffect 清理函数中的所有订阅和异步任务?

在反应中使用异步等待从firebase存储中检索图像

while 循环在 useEffect 挂钩中不起作用

如何在 useEffect 挂钩中使用 Promise.all 获取所有数据?

异步等待不等待 useEffect 内的响应