当我不再需要时,如何清理这个 `await nextEvent(element, 'mousemove')` 模式?
How to clean up this `await nextEvent(element, 'mousemove')` pattern when I no longer need it?
我有一个包含如下代码的 React 组件:
class MyComponent extends React.Component {
// ...
trackStats = false
componentDidMount() {
this.monitorActivity()
}
componentWillUnmount() {
this.trackStats = false
}
async monitorActivity() {
this.trackStats = true
while (this.trackStats && this.elRef.current) {
// elRef is a React ref to a DOM element rendered in render()
await Promise.race([
nextEvent(this.elRef.current, 'keydown'),
nextEvent(this.elRef.current, 'click'),
nextEvent(this.elRef.current, 'mousemove'),
nextEvent(this.elRef.current, 'pointermove'),
])
this.logUserActivity()
}
}
logUserActivity() {
// ...
}
render() { /* ... */ }
}
const nextEvent = (target, eventName) => new Promise(resolve => {
target.addEventListener(eventName, resolve, { once: true })
})
问题是,如果卸载此组件,那么添加到 this.elRef.current
引用的 DOM 元素上的事件处理程序将保留在内存中,因为用户将不再进行交互使用不再在 DOM.
中的元素
所以 while 循环将卡住等待下一个事件,这永远不会发生,并且因为 while 循环仍在等待最后一个事件,我相信这会导致实例 MyComponent
在内存中泄漏。
或者引擎是否足够聪明来清理它?如果我没有对这些东西的任何可访问引用,并且唯一链接的是 while 循环的范围,它正在等待一些承诺的实现,那么引擎会丢弃它吗?或者它会离开 while 循环范围 运行,等待 Promises 吗?
如果 while 循环仍然存在(我猜它确实存在),我应该如何清理它?
啊有趣的用例!这似乎是 AbortController:
的一个很好的用例
function nextEvent(target, type, abortSignal) {
return new Promise(resolve => {
target.addEventListener(type, resolve, { once: true });
abortSignal.addEventListener("abort", () =>
target.removeEventListener(type, resolve)
);
});
}
const abortController = new AbortController();
const event = await Promise.race([
nextEvent(someButton, "click", abortController.signal),
nextEvent(someButton, "keydown", abortController.signal)
]);
// Clean up all remaining event handlers
abortController.abort();
// Continue as normal
感谢 Surma 的指导,我想出了一个在卸载组件时彻底清理的方法:
class MyComponent extends React.Component {
// ...
trackStats = false
statsAbort = undefined
componentDidMount() {
this.monitorActivity()
}
componentWillUnmount() {
this.trackStats = false
this.statsAbort.abort()
}
async monitorActivity() {
this.trackStats = true
while (this.trackStats && this.elRef.current) {
this.statsAbort = new AbortController
try {
// elRef is a React ref to a DOM element rendered in render()
await Promise.race([
nextEvent(this.elRef.current, 'keydown'),
nextEvent(this.elRef.current, 'click'),
nextEvent(this.elRef.current, 'mousemove'),
nextEvent(this.elRef.current, 'pointermove'),
])
} catch(e) {
if (e.message !== 'abort_stats') throw e
}
this.statsAbort.abort()
this.logUserActivity()
}
}
logUserActivity() {
// ...
}
render() { /* ... */ }
}
const nextEvent = (target, eventName, abortSignal) => new Promise((resolve, reject) => {
target.addEventListener(eventName, resolve, { once: true })
abortSignal.addEventListener("abort", () => {
target.removeEventListener(eventName, resolve)
reject(new Error('abort_stats'))
});
})
但是直接使用 addEventListener 更简单,所以我选择了以下对于这个用例也更容易理解的方法:
class MyComponent extends React.Component {
// ...
componentDidMount() {
const el = this.elRef.current
el.addEventListener('keydown', this.logUserActivity)
el.addEventListener('click', this.logUserActivity)
el.addEventListener('mousemove', this.logUserActivity)
el.addEventListener('pointermove', this.logUserActivity)
}
componentWillUnmount() {
const el = this.elRef.current
el.removeEventListener('keydown', this.logUserActivity)
el.removeEventListener('click', this.logUserActivity)
el.removeEventListener('mousemove', this.logUserActivity)
el.removeEventListener('pointermove', this.logUserActivity)
}
logUserActivity() {
// ...
}
render() { /* ... */ }
}
我有一个包含如下代码的 React 组件:
class MyComponent extends React.Component {
// ...
trackStats = false
componentDidMount() {
this.monitorActivity()
}
componentWillUnmount() {
this.trackStats = false
}
async monitorActivity() {
this.trackStats = true
while (this.trackStats && this.elRef.current) {
// elRef is a React ref to a DOM element rendered in render()
await Promise.race([
nextEvent(this.elRef.current, 'keydown'),
nextEvent(this.elRef.current, 'click'),
nextEvent(this.elRef.current, 'mousemove'),
nextEvent(this.elRef.current, 'pointermove'),
])
this.logUserActivity()
}
}
logUserActivity() {
// ...
}
render() { /* ... */ }
}
const nextEvent = (target, eventName) => new Promise(resolve => {
target.addEventListener(eventName, resolve, { once: true })
})
问题是,如果卸载此组件,那么添加到 this.elRef.current
引用的 DOM 元素上的事件处理程序将保留在内存中,因为用户将不再进行交互使用不再在 DOM.
所以 while 循环将卡住等待下一个事件,这永远不会发生,并且因为 while 循环仍在等待最后一个事件,我相信这会导致实例 MyComponent
在内存中泄漏。
或者引擎是否足够聪明来清理它?如果我没有对这些东西的任何可访问引用,并且唯一链接的是 while 循环的范围,它正在等待一些承诺的实现,那么引擎会丢弃它吗?或者它会离开 while 循环范围 运行,等待 Promises 吗?
如果 while 循环仍然存在(我猜它确实存在),我应该如何清理它?
啊有趣的用例!这似乎是 AbortController:
的一个很好的用例function nextEvent(target, type, abortSignal) {
return new Promise(resolve => {
target.addEventListener(type, resolve, { once: true });
abortSignal.addEventListener("abort", () =>
target.removeEventListener(type, resolve)
);
});
}
const abortController = new AbortController();
const event = await Promise.race([
nextEvent(someButton, "click", abortController.signal),
nextEvent(someButton, "keydown", abortController.signal)
]);
// Clean up all remaining event handlers
abortController.abort();
// Continue as normal
感谢 Surma 的指导,我想出了一个在卸载组件时彻底清理的方法:
class MyComponent extends React.Component {
// ...
trackStats = false
statsAbort = undefined
componentDidMount() {
this.monitorActivity()
}
componentWillUnmount() {
this.trackStats = false
this.statsAbort.abort()
}
async monitorActivity() {
this.trackStats = true
while (this.trackStats && this.elRef.current) {
this.statsAbort = new AbortController
try {
// elRef is a React ref to a DOM element rendered in render()
await Promise.race([
nextEvent(this.elRef.current, 'keydown'),
nextEvent(this.elRef.current, 'click'),
nextEvent(this.elRef.current, 'mousemove'),
nextEvent(this.elRef.current, 'pointermove'),
])
} catch(e) {
if (e.message !== 'abort_stats') throw e
}
this.statsAbort.abort()
this.logUserActivity()
}
}
logUserActivity() {
// ...
}
render() { /* ... */ }
}
const nextEvent = (target, eventName, abortSignal) => new Promise((resolve, reject) => {
target.addEventListener(eventName, resolve, { once: true })
abortSignal.addEventListener("abort", () => {
target.removeEventListener(eventName, resolve)
reject(new Error('abort_stats'))
});
})
但是直接使用 addEventListener 更简单,所以我选择了以下对于这个用例也更容易理解的方法:
class MyComponent extends React.Component {
// ...
componentDidMount() {
const el = this.elRef.current
el.addEventListener('keydown', this.logUserActivity)
el.addEventListener('click', this.logUserActivity)
el.addEventListener('mousemove', this.logUserActivity)
el.addEventListener('pointermove', this.logUserActivity)
}
componentWillUnmount() {
const el = this.elRef.current
el.removeEventListener('keydown', this.logUserActivity)
el.removeEventListener('click', this.logUserActivity)
el.removeEventListener('mousemove', this.logUserActivity)
el.removeEventListener('pointermove', this.logUserActivity)
}
logUserActivity() {
// ...
}
render() { /* ... */ }
}