在 useEffect 挂钩中取消所有 async/await 任务以防止内存泄漏的正确方法是什么?
What is the right way to cancel all async/await tasks within an useEffect hook to prevent memory leaks in react?
我正在开发一个从 firebase 数据库中提取数据的 React Chap 应用程序。在我的 "Dashboard" 组件中,我有一个 useEffect 钩子来检查经过身份验证的用户,如果是,则从 firebase 中提取数据并设置电子邮件变量和聊天变量的状态。我使用 abortController 进行 useEffect 清理,但是每当我第一次注销并重新登录时,我都会收到内存泄漏警告。
index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in Dashboard (created by 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]);
预期结果是 cleanup/cancel useEffect 清理函数中的所有异步任务。在一个用户注销后,相同或不同的用户重新登录,我在控制台中收到以下警告
index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions
and asynchronous tasks in a useEffect cleanup function.
in Dashboard (created by Context.Consumer)
onSnapshot
method不是return一个承诺,所以等待它的结果是没有意义的。相反,它 开始 侦听数据(以及对该数据的更改),并使用相关数据调用 onSnapshot
回调。这可能会发生多次,因此它不能 return 一个承诺。在您通过调用从 onSnapshot
编辑的 return 方法取消订阅之前,侦听器会一直连接到数据库。由于您从不存储该方法,更不用说调用它了,因此侦听器保持活动状态,稍后将再次调用您的回调。这可能是内存泄漏的来源。
如果您想等待 Firestore 的结果,您可能正在寻找 get()
method。这样获取一次数据,然后解决promise。
await firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.get(async res => {
取消 async/await
的一种方法是创建类似于内置 AbortController
的东西,它将 return 两个函数:一个用于取消,一个用于检查取消,然后在之前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>
在 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 数据库中提取数据的 React Chap 应用程序。在我的 "Dashboard" 组件中,我有一个 useEffect 钩子来检查经过身份验证的用户,如果是,则从 firebase 中提取数据并设置电子邮件变量和聊天变量的状态。我使用 abortController 进行 useEffect 清理,但是每当我第一次注销并重新登录时,我都会收到内存泄漏警告。
index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in Dashboard (created by 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]);
预期结果是 cleanup/cancel useEffect 清理函数中的所有异步任务。在一个用户注销后,相同或不同的用户重新登录,我在控制台中收到以下警告
index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in Dashboard (created by Context.Consumer)
onSnapshot
method不是return一个承诺,所以等待它的结果是没有意义的。相反,它 开始 侦听数据(以及对该数据的更改),并使用相关数据调用 onSnapshot
回调。这可能会发生多次,因此它不能 return 一个承诺。在您通过调用从 onSnapshot
编辑的 return 方法取消订阅之前,侦听器会一直连接到数据库。由于您从不存储该方法,更不用说调用它了,因此侦听器保持活动状态,稍后将再次调用您的回调。这可能是内存泄漏的来源。
如果您想等待 Firestore 的结果,您可能正在寻找 get()
method。这样获取一次数据,然后解决promise。
await firebase
.firestore()
.collection('chats')
.where('users', 'array-contains', _user.email)
.get(async res => {
取消 async/await
的一种方法是创建类似于内置 AbortController
的东西,它将 return 两个函数:一个用于取消,一个用于检查取消,然后在之前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>
在 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