你所拥有的将为第一的渲染,但不是后续的。(如果你使用 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 是性能优化,不是语义保证。 useCallback 是 useMemo 的包装,其中包含此免责声明(他们的重点):"您可能依赖 useMemo 作为性能优化,而不是语义保证。"但是您的代码依赖它作为语义保证。)