Как очистить этот шаблон `await nextEvent (element, 'mousemove')` `, когда он мне больше не нужен? - PullRequest
0 голосов
/ 04 января 2019

У меня есть компонент 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 })
})

Проблема в том, что если этот компонент отключен, то обработчики событий, которые добавляются в элемент DOM, на который ссылается this.elRef.current останется в памяти, так как пользователь больше не будет взаимодействовать с элементом, которого больше нет в DOM.

Так что цикл while будет зависать в ожидании следующих событий, которые никогда не произойдут, ипоскольку цикл while все еще ожидает одно последнее событие, я полагаю, что это приведет к утечке экземпляра MyComponent в память.

Или движок достаточно умен, чтобы это очистить?Если у меня нет доступных ссылок на какие-либо из этих вещей, и единственное, что связано, - это область цикла while, которая ожидает выполнения некоторых обещаний, каждый движок откажется от нее?Или он оставит работоспособным цикл while, ожидая Обещания?

Если цикл while остается (что, я предполагаю, он делает), как мне его очистить?

Ответы [ 2 ]

0 голосов
/ 06 января 2019

Благодаря руководству 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() { /* ... */ }
}
0 голосов
/ 04 января 2019

Ах, интересный вариант использования!Это похоже на отличный пример использования 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
...