【问题标题】:How to clean up this `await nextEvent(element, 'mousemove')` pattern when I no longer need it?当我不再需要这个 `await nextEvent(element, 'mousemove')` 模式时,如何清理它?
【发布时间】:2019-01-04 04:17:42
【问题描述】:

我有一个 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 循环范围运行,等待 Promise?

如果 while 循环仍然存在(我猜它确实存在),我应该如何清理它?

【问题讨论】:

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


    【解决方案1】:

    啊有趣的用例!这似乎是 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
    

    【讨论】:

    • abort 在这些行中指的是什么:nextEvent(someButton, "click", abort.signal)
    • @artem 应该是abortController,我做了修改。
    【解决方案2】:

    感谢 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() { /* ... */ }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-19
      • 1970-01-01
      • 1970-01-01
      • 2020-08-31
      • 2019-05-07
      • 1970-01-01
      相关资源
      最近更新 更多