在 useEffect 钩子中取消所有异步/等待任务以防止react中的内存泄漏的正确方法是什么?

IT技术 reactjs firebase async-await google-cloud-firestore use-effect
2021-04-30 10:46:26

我正在开发一个从 firebase 数据库中提取数据的 react chap 应用程序。在我的“仪表板”组件中,我有一个 useEffect 钩子检查经过身份验证的用户,如果是,则从 firebase 中提取数据并设置电子邮件变量和聊天变量的状态。我使用 abortController 进行 useEffect 清理,但是每当我第一次注销并重新登录时,我都会收到内存泄漏警告。

index.js:1375 警告:无法对卸载的组件执行 React 状态更新。这是一个空操作,但它表明您的应用程序中存在内存泄漏。要修复,请取消 useEffect 清理函数中的所有订阅和异步任务。

在仪表板中(由 Context.Consumer 创建)

最初我没有 abortController,我只是在清理时返回了一个控制台日志。做了更多研究并发现 abortController 但是示例使用了 fetch 和信号,我找不到任何与 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 创建)

3个回答

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

onSnapshot方法不会返回一个Promise,所以有在等待它的结果没有任何意义。相反,它开始侦听数据(以及对该数据的更改),并onSnapshot使用相关数据调用回调。这可能发生多次,因此它无法返回Promise。侦听器会一直连接到数据库,直到您通过调用从 返回的方法取消订阅它onSnapshot由于您从不存储该方法,更不用说调用它,因此侦听器保持活动状态,稍后将再次调用您的回调。这很可能是内存泄漏的来源。

如果您想等待 Firestore 的结果,您可能正在寻找get()method这将获取一次数据,然后解析Promise。

await firebase
      .firestore()
      .collection('chats')
      .where('users', 'array-contains', _user.email)
      .get(async res => {

取消的一种方法async/await是创建类似于 built-in 的东西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>