当我不再需要这种`await nextEvent(element,'mousemove')`模式时,该如何清理呢?

时间:2019-01-04 04:17:43

标签: javascript reactjs typescript memory-leaks code-cleanup

我有一个带有以下代码的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循环仍然存在(我猜是这样),我应该如何清理呢?

2 个答案:

答案 0 :(得分:2)

一个有趣的用例!对于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

答案 1 :(得分:1)

由于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() { /* ... */ }
}