有没有更好的方法来避免 React 钩子中的陈旧闭包?
Is there any better way to avoid stale closures in React hooks?
假设我们要实现这些要求:
- 在按住鼠标移动时显示光标位置。
- 下次按下鼠标时,将上次释放鼠标时的最后一个光标位置记录到控制台。
这是我的解决方案,效果很好。但我对此不太满意:
https://codesandbox.io/s/funny-orla-kdx19?file=/src/App.js
import React, { useState, useEffect } from "react";
import "./styles.css";
const html = document.documentElement;
export default function App() {
const [cursorPos, setCursorPos] = useState({});
useEffect(() => {
const pointerMove = (e) => {
setCursorPos({ x: e.clientX, y: e.clientY });
};
const pointerUp = (e) => {
html.removeEventListener("pointermove", pointerMove);
html.removeEventListener("pointerup", pointerUp);
};
const pointerDown = () => {
console.log("Last position when pointer up", cursorPos);
html.addEventListener("pointermove", pointerMove);
html.addEventListener("pointerup", pointerUp);
};
console.log("registering pointerdown callback");
html.addEventListener("pointerdown", pointerDown);
return () => {
html.removeEventListener("pointerdown", pointerDown);
};
}, [cursorPos]);
return (
<div className="App">
Tracked mouse position: ({cursorPos.x}, {cursorPos.y})
</div>
);
}
如您所见,cursorPos 依赖项至关重要。没有它,pointerDown 侦听器将不会读取最新的跟踪光标位置。但是,由于 cursorPos 由 pointermove 事件更新,因此它会导致效果回调被调用得如此频繁。并且,pointerdown 侦听器结果是 deregister/register。
侦听器的这种频繁重新注册可能不会造成实际问题。但这在概念上并不是一种巧妙的做事方式。如果可能的话,这是一种开销。这就是我不满意的原因。
我实际上想出了一些替代方案,但这些替代方案也没有让我感到满足。
解决方案 1:
https://codesandbox.io/s/mystifying-lamport-nr3jk?file=/src/App.js
我们可以在每次鼠标松开时记录光标位置,并将其存储在另一个状态变量 (cursorPosWhenMouseUp) 中,下一次鼠标按下事件将读取该变量。由于鼠标松开事件不是那么频繁触发,重新注册的频率是可以接受的。这个解决方案的缺点是 cursorPosWhenMouseUp 变量对我来说是多余的,因为最后一个 cursorPos 值应该是相同的。其唯一目的是缓解技术问题。让它留在代码中并不优雅。
解决方案 2:
https://codesandbox.io/s/upbeat-shamir-rhjs4?file=/src/App.js
我们可以将 cursorPos 的另一个副本存储在 useRef() 的引用中(例如 cursorPosRef)。由于 ref 在整个渲染过程中是稳定的,并且不涉及局部变量,因此我们可以放心地从 pointerDown 回调中安全地读取它。此解决方案与解决方案 1 存在类似问题,因为我们必须仔细确保 cursorPosRef 始终反映 cursorPos 值。
解决方案 3:
https://codesandbox.io/s/polished-river-psvz7?file=/src/App.js
针对上面提到的冗余问题,我们可以放弃cursorPos状态变量,只使用cursorPosRef。我们可以直接在 JSX 中读取它。我们只需要一种方法来从 pointermove 回调中强制更新组件。 React Hooks FAQ 阐明了这一点。这个解决方案的问题是在 React 方法中不欢迎 forceUpdate 东西。
那么,有没有更好的方法来达到这些要求呢?
我认为不需要将 cursorPos
作为依赖项传递,这是我的尝试:
import React, { useEffect, useState } from 'react';
import './styles.css';
const html = document.documentElement;
export default function App() {
const [cursorPos, setCursorPos] = useState({});
const pointerMove = e => {
setCursorPos({ x: e.clientX, y: e.clientY });
};
useEffect(() => {
html.addEventListener('pointerdown', e => {
html.addEventListener('pointermove', pointerMove);
});
html.addEventListener('pointerup', e => {
html.removeEventListener('pointermove', pointerMove);
});
return () => {
html.removeEventListener('pointerdown');
html.removeEventListener('pointerup');
};
}, []);
return (
<div className="App">
Tracked mouse position: ({cursorPos.x}, {cursorPos.y})
</div>
);
}
https://codesandbox.io/s/eloquent-mendeleev-wqzk4?file=/src/App.js
更新:
为了跟踪之前的位置,我会使用 useRef:
import React, { useEffect, useRef, useState } from 'react';
const html = document.documentElement;
export default function Test() {
const [cursorCurrentPos, setCursorCurrentPos] = useState({});
const cursorPreviousPos = useRef({});
const pointerMove = e => {
setCursorCurrentPos({ x: e.clientX, y: e.clientY });
};
useEffect(() => {
html.addEventListener('pointerdown', e => {
console.log('Last position when pointer up', cursorPreviousPos.current);
html.addEventListener('pointermove', pointerMove);
});
html.addEventListener('pointerup', e => {
cursorPreviousPos.current = { x: e.clientX, y: e.clientY };
html.removeEventListener('pointermove', pointerMove);
});
return () => {
html.removeEventListener('pointerdown');
html.removeEventListener('pointerup');
};
}, []);
return (
<div className="App">
Tracked mouse position: ({cursorCurrentPos.x}, {cursorCurrentPos.y})
</div>
);
}
假设我们要实现这些要求:
- 在按住鼠标移动时显示光标位置。
- 下次按下鼠标时,将上次释放鼠标时的最后一个光标位置记录到控制台。
这是我的解决方案,效果很好。但我对此不太满意: https://codesandbox.io/s/funny-orla-kdx19?file=/src/App.js
import React, { useState, useEffect } from "react";
import "./styles.css";
const html = document.documentElement;
export default function App() {
const [cursorPos, setCursorPos] = useState({});
useEffect(() => {
const pointerMove = (e) => {
setCursorPos({ x: e.clientX, y: e.clientY });
};
const pointerUp = (e) => {
html.removeEventListener("pointermove", pointerMove);
html.removeEventListener("pointerup", pointerUp);
};
const pointerDown = () => {
console.log("Last position when pointer up", cursorPos);
html.addEventListener("pointermove", pointerMove);
html.addEventListener("pointerup", pointerUp);
};
console.log("registering pointerdown callback");
html.addEventListener("pointerdown", pointerDown);
return () => {
html.removeEventListener("pointerdown", pointerDown);
};
}, [cursorPos]);
return (
<div className="App">
Tracked mouse position: ({cursorPos.x}, {cursorPos.y})
</div>
);
}
如您所见,cursorPos 依赖项至关重要。没有它,pointerDown 侦听器将不会读取最新的跟踪光标位置。但是,由于 cursorPos 由 pointermove 事件更新,因此它会导致效果回调被调用得如此频繁。并且,pointerdown 侦听器结果是 deregister/register。
侦听器的这种频繁重新注册可能不会造成实际问题。但这在概念上并不是一种巧妙的做事方式。如果可能的话,这是一种开销。这就是我不满意的原因。
我实际上想出了一些替代方案,但这些替代方案也没有让我感到满足。
解决方案 1:
https://codesandbox.io/s/mystifying-lamport-nr3jk?file=/src/App.js
我们可以在每次鼠标松开时记录光标位置,并将其存储在另一个状态变量 (cursorPosWhenMouseUp) 中,下一次鼠标按下事件将读取该变量。由于鼠标松开事件不是那么频繁触发,重新注册的频率是可以接受的。这个解决方案的缺点是 cursorPosWhenMouseUp 变量对我来说是多余的,因为最后一个 cursorPos 值应该是相同的。其唯一目的是缓解技术问题。让它留在代码中并不优雅。
解决方案 2:
https://codesandbox.io/s/upbeat-shamir-rhjs4?file=/src/App.js
我们可以将 cursorPos 的另一个副本存储在 useRef() 的引用中(例如 cursorPosRef)。由于 ref 在整个渲染过程中是稳定的,并且不涉及局部变量,因此我们可以放心地从 pointerDown 回调中安全地读取它。此解决方案与解决方案 1 存在类似问题,因为我们必须仔细确保 cursorPosRef 始终反映 cursorPos 值。
解决方案 3:
https://codesandbox.io/s/polished-river-psvz7?file=/src/App.js
针对上面提到的冗余问题,我们可以放弃cursorPos状态变量,只使用cursorPosRef。我们可以直接在 JSX 中读取它。我们只需要一种方法来从 pointermove 回调中强制更新组件。 React Hooks FAQ 阐明了这一点。这个解决方案的问题是在 React 方法中不欢迎 forceUpdate 东西。
那么,有没有更好的方法来达到这些要求呢?
我认为不需要将 cursorPos
作为依赖项传递,这是我的尝试:
import React, { useEffect, useState } from 'react';
import './styles.css';
const html = document.documentElement;
export default function App() {
const [cursorPos, setCursorPos] = useState({});
const pointerMove = e => {
setCursorPos({ x: e.clientX, y: e.clientY });
};
useEffect(() => {
html.addEventListener('pointerdown', e => {
html.addEventListener('pointermove', pointerMove);
});
html.addEventListener('pointerup', e => {
html.removeEventListener('pointermove', pointerMove);
});
return () => {
html.removeEventListener('pointerdown');
html.removeEventListener('pointerup');
};
}, []);
return (
<div className="App">
Tracked mouse position: ({cursorPos.x}, {cursorPos.y})
</div>
);
}
https://codesandbox.io/s/eloquent-mendeleev-wqzk4?file=/src/App.js
更新:
为了跟踪之前的位置,我会使用 useRef:
import React, { useEffect, useRef, useState } from 'react';
const html = document.documentElement;
export default function Test() {
const [cursorCurrentPos, setCursorCurrentPos] = useState({});
const cursorPreviousPos = useRef({});
const pointerMove = e => {
setCursorCurrentPos({ x: e.clientX, y: e.clientY });
};
useEffect(() => {
html.addEventListener('pointerdown', e => {
console.log('Last position when pointer up', cursorPreviousPos.current);
html.addEventListener('pointermove', pointerMove);
});
html.addEventListener('pointerup', e => {
cursorPreviousPos.current = { x: e.clientX, y: e.clientY };
html.removeEventListener('pointermove', pointerMove);
});
return () => {
html.removeEventListener('pointerdown');
html.removeEventListener('pointerup');
};
}, []);
return (
<div className="App">
Tracked mouse position: ({cursorCurrentPos.x}, {cursorCurrentPos.y})
</div>
);
}