【问题标题】:How to remove an eventListener in React?如何在 React 中删除 eventListener?
【发布时间】:2022-11-11 02:27:50
【问题描述】:

我知道这个主题是一个古老的主题。但我认为我有一些新的东西,或者至少我找不到具有这些特征的任何问题。我无法删除eventListener,即使是React.useCallback。那么,我现在该怎么办?

这是我的代码,在我的组件类的开头:

const noCursorEventListener = React.useCallback((e) => {
    console.log('ncel');
    let lista = document.getElementsByClassName('lista');
    if (lista && lista[0]) lista[0].classList.remove('nocursor');
}, []);

window.addEventListener('mousemove', noCursorEventListener);

我用来删除它的 useEffect :

useEffect(() => {
    return () => {
        window.removeEventListener('mousemove', noCursorEventListener);
        window.onmousemove = null;
        console.log('remove el');
    }
});

我正确地看到了remove el,但在那之后和页面更改之后,我仍然收到ncel 消息。有任何想法吗? window.onmousemove = null 不应该是必要的。是一个失败的测试。

【问题讨论】:

    标签: javascript reactjs react-hooks event-listener


    【解决方案1】:

    你所拥有的将为第一的渲染,但不是后续的。(如果你使用 React 的 StrictMode,它可能在一开始就渲染了两次。)您可以看到为什么我们在发生的每个阶段都记录一条消息(我已将 mousemove 更改为 click,因为这无关紧要,并且避免了混乱的日志):

    const { useState, useEffect } = React;
    
    const Example = () => {
        const noCursorEventListener = React.useCallback((e) => {
            console.log("callback called!");
        }, []);
    
        console.log("Adding callback");
        window.addEventListener("click", noCursorEventListener);
    
        useEffect(() => {
            return () => {
                console.log("Removing callback");
                window.removeEventListener("click", noCursorEventListener);
            };
        });
    
        const [counter, setCounter] = useState(0);
        const increment = (event) => {
            setCounter(c => c + 1);
            event.stopPropagation();
        };
    
        return (
            <div>
                {counter} <input type="button" value="+" onClick={() => setCounter((c) => c + 1)} />
            </div>
        );
    };
    
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<Example />);
    <div id="root"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

    如果你运行它,你会看到Adding callback,因为渲染添加了 回调。如果您单击按钮以外的其他位置,您将看到callback called!。但是,如果您单击该按钮以进行第二次渲染,您将看到以下序列:

    添加回调
    删除回调

    注意顺序.它重新添加回调(它不做任何事情,因为您不能将同一事件的同一事件侦听器多次添加到同一元素),然后在渲染之后为 useEffect 清理以前的渲染运行,删除回调。这在useEffect cleanup 的工作方式中是隐含的,但看起来有点令人惊讶。

    有趣的是,如果你不是记住回调,它会起作用,因为在添加时,它会短暂添加第二个回调,然后第一个回调将被useEffect 清理删除。

    const { useState, useEffect } = React;
    
    const Example = () => {
        const noCursorEventListener = /*React.useCallback(*/(e) => {
            console.log("callback called!");
        }/*, [])*/;
    
        console.log("Adding callback");
        window.addEventListener("click", noCursorEventListener);
    
        useEffect(() => {
            return () => {
                console.log("Removing callback");
                window.removeEventListener("click", noCursorEventListener);
            };
        });
    
        const [counter, setCounter] = useState(0);
        const increment = (event) => {
            setCounter(c => c + 1);
            event.stopPropagation();
        };
    
        return (
            <div>
                {counter} <input type="button" value="+" onClick={() => setCounter((c) => c + 1)} />
            </div>
        );
    };
    
    const root = ReactDOM.createRoot(document.getElementById("root"));
    root.render(<Example />);
    <div id="root"></div>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

    不要那样做.除了调用钩子,你的render 函数应该是纯的(它不应该有有意义的副作用)。添加事件侦听器是一个有意义的副作用。

    副作用是useEffect (more here) 的重点。所以让我们以标准方式来做,通过在useEffect 回调中连接侦听器并在完成该效果的清理时删除相同的处理程序。 (这也意味着我们不会在每次丢弃时都创建新的侦听器函数。)这是标准方法:

    useEffect(() => {
        const noCursorEventListener = (e) => {
            let lista = document.getElementsByClassName("lista");
            if (lista && lista[0]) lista[0].classList.remove("nocursor");
        };
    
        window.addEventListener("mousemove", noCursorEventListener);
        return () => {
            window.removeEventListener("mousemove", noCursorEventListener);
        };
    }, []); // <== Empty dependencies array = only run effect on mount
    

    (还有一个单独的问题:useCallback性能优化,不是语义保证。 useCallbackuseMemo 的包装,其中包含此免责声明(他们的重点)"您可能依赖 useMemo 作为性能优化,而不是语义保证。"但是您的代码依赖它作为语义保证。)

    【讨论】:

    • 发挥魅力。谢谢。我仍然习惯于 React 的装饰 :D
    • 虽然这肯定是更好的方法,但我想知道为什么原始代码不会。记忆函数是const。在有另一个渲染之前,它不应该被垃圾收集或重新计算,因此清理回调引用的函数应该始终是在最后一次渲染开始时添加的函数。有任何想法吗?
    • @CertainPerformance - 有趣的是,问题在于稳定性(嗯,在渲染期间执行非挂钩副作用)。第二个渲染在第一个渲染的清理发生之前运行。由于监听器是稳定的,第二次调用addEventListener 并没有做任何事情,但清理成功将其移除。 :-)
    猜你喜欢
    • 1970-01-01
    • 2020-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-10
    • 2023-03-14
    • 2021-06-20
    • 2022-01-10
    相关资源
    最近更新 更多