我不能删除
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
不应该是必要的。这是一个失败的测试。
TL;DR 不要在渲染中做(非挂钩)副作用。 :-) 对于潜伏者,如果您只是在寻找如何使用 DOM 方法正确添加事件侦听器(在极少数情况下是合适的),请参阅下面的标准方式。
但是对于那些对为什么 OP 的代码不起作用感兴趣的人:
您所拥有的将用于first渲染,但不适用于后续渲染。 (如果您使用的是 React 的
StrictMode
,它可能一开始就渲染了两次。) 如果我们在发生的每个阶段都记录一条消息,您就会明白为什么(我已经将 mousemove
更改为 click
因为这无关紧要,并且可以避免使日志混乱):
const { useState, useEffect } = React;
const Example = () => {
const noCursorEventListener = React.useCallback((e) => {
console.log("event listener called!");
}, []);
console.log("Adding event listener");
window.addEventListener("click", noCursorEventListener);
useEffect(() => {
return () => {
console.log("Removing event listener");
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 event listener
因为渲染添加了事件监听器。如果你点击按钮以外的地方,你会看到event listener called!
。但是如果你点击按钮进行第二次渲染,你会看到这个序列:
添加事件监听器 删除事件侦听器
注意顺序。它重新添加事件侦听器(它不执行任何操作,因为您不能多次将同一事件的相同事件侦听器函数添加到同一元素),然后在渲染之后进行
useEffect
清理previous 渲染运行,删除事件侦听器。这隐含在 useEffect
cleanup 的工作方式中,但它看起来有点令人惊讶。
有趣的是,如果您 没有 记住事件监听器,它会起作用,因为在添加时,它会短暂地添加第二个事件监听器,然后第一个会被
useEffect
清理删除。
const { useState, useEffect } = React;
const Example = () => {
const noCursorEventListener = /*React.useCallback(*/(e) => {
console.log("event listener called!");
}/*, [])*/;
console.log("Adding event listener");
window.addEventListener("click", noCursorEventListener);
useEffect(() => {
return () => {
console.log("Removing event listener");
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
的重点(更多信息)。因此,让我们以标准方式进行操作,通过在 useEffect
回调中连接侦听器并在完成该效果的清理后删除相同的处理程序。 (这也意味着我们不会在每次丢弃时都创建一个新的监听器函数。)
这是在挂载时添加事件侦听器并在卸载时删除它的标准方法,对于那些直接使用 DOM 执行此操作的相对罕见的用例是合适的:
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
是性能优化,而不是语义保证。useCallback
是useMemo
的包装,它有这个免责声明(他们的重点): “您可能依赖 useMemo 作为性能优化,而不是语义保证。” 但您的代码依赖它作为语义保证。)