【发布时间】:2020-06-04 16:18:13
【问题描述】:
我的组件似乎进入了无限循环,我找不到原因。我正在使用useEffect 和useState 钩子,但事实并非如此。 致任何想要将其标记为重复的人:请仔细阅读问题。这种无休止的重新渲染循环场景仅在以下情况下才会发生:
- 鼠标移动得太快
- 用户单击开发者控制台中的任何内容,然后触发
mousemove事件(为了清楚起见,我省略了mousedown/mouseup处理程序,但它们与@ 基本相同987654329@ 处理程序,触发对每个订阅回调的调用。
这是一个示例,但请注意它可能会导致无限循环并迫使您关闭浏览器选项卡 代码沙盒: https://codesandbox.io/s/infiniteloopissue-lynhw?file=/index.js
我正在使用Map.forEach 触发mousemove 事件上任何订阅者的setState。 (你可以在下面看到相关代码),我正在使用useEffect订阅/取消订阅这个“更新事件”
“解决”问题的一件事如下 - 检查参考点注释(在鼠标使用者下方)。如果回调作为 useCallback 挂钩的依赖项被删除,则一切正常。但这并不好,因为在这个例子中,我们显然只是将数据提取到状态中,但是 callback 函数可能依赖于其他状态,在这种情况下它将不起作用。回调需要是可变的。
我的猜测是,在 .forEach 完成迭代之前,react 以某种方式设法重新渲染,在这种情况下,它会取消订阅(从而删除密钥)并重新订阅(从而再次添加它)触发另一个回调,然后触发另一个取消订阅/重新订阅,我们进入一个循环。但这应该是不可能的吧?我的意思是 javascrip 应该是阻塞单线程的,如何/为什么在 forEach 循环中间重新渲染?
另外,是否有人对如何“订阅”mousemove 并运行回调有更好的想法。我最近在一些后端代码中看到了一些EventEmitter,但对它并不熟悉。我也不确定这是否可以解决这里的问题,在更新时反应问题优先于等待主线程完成 .forEach 循环(至少我认为是这样)
基础很简单:
App.js
const App = () => {
return (
<MouseProvider>
<Component />
</MouseProvider>
)
}
组件.js
const Component = props => {
const [mouse, setMouse] = useState({})
const callback = data => {
setMouse({ x: data.x, y: data.y })
}
useMouseTracker(callback)
return (
<div>
{`x: ${mouse.x} y: ${mouse.y}`}
</div>
)
}
组件背后的想法是,始终记下当前鼠标在屏幕上的位置。这些信息在上下文中很容易获得,但是为了在屏幕上呈现它,我们需要触发“重新渲染”,因此 Context API 不是一个解决方案,而是。
鼠标提供者
// Static mutable object used.
const mouseData = { x: 0, y: 0 }
// A map of "id : callback" pairs
const subscribers = new Map()
Provider = ({ children }) => {
const mouseMoveHandler = useCallback(event => {
if (event) {
mouseData.x = event.clientX
mouseData.y = event.clientY
subscribers.forEach(callback => {
callback({ ...mouseData})
})
}
}, [])
useEffect(() => {
window.addEventListener('mousemove', mouseMoveHandler)
return () => {
window.removeEventListener('mousemove', mouseMoveHandler)
}
}, [mouseMoveHandler])
return (
<React.Fragment>
{children}
</React.Fragment>
)
}
所以每次用户移动鼠标时,mousemove 处理程序都会更新static 对象。 Provider 组件本身不会重新渲染。
鼠标消费者
useMouseTracker = callback => {
const id = 0 // This value is not 0, it's a system-wide per-component constant, guaranteed, tried and tested
const subscribe = useCallback(() => {
subscribers.set(id, callback)
}, [id, callback /* Reference Point */])
const unsubscribe = useCallback(() => {
subscribers.delete(id)
}, [id])
useEffect(() => {
subscribe()
return unsubscribe
}, [subscribe, unsubscribe])
}
正如我们所见,Consumer Hook 实现了两个函数,分别是 subscribe 和 unsubscribe 和 id 到回调的 Map 中,之前在 Provider 中引用过,这就是它所做的一切,它没有调用回调,除了从 Map 对象中添加/删除 callback 之外,它永远不会触发任何事情。
所有的“更新”都是由 Provider 或者更确切地说是回调提供者调用的组件在每个mousemove 上完成的。换句话说,useEffect 不会触发状态更新,鼠标交互会。
useEffect 钩子返回一个取消订阅函数,该函数确保它“自行清理”,以便在卸载组件时不会触发 callback。
问题
正如我所说的那样,问题是,我最终会使用这个组件进行无休止的重新渲染循环,并且只有当鼠标移动得太快或者它“离开屏幕”(例如进入开发者控制台)时才会发生这种情况,例如。
编辑:完全删除上下文,没有必要并且造成混乱。 EDIT2:添加了代码框
【问题讨论】:
-
我认为最好使用render prop 这不是上下文。
-
你的 mousemove 回调钩子取决于注意,所以它似乎只是记住了第一个参数。嗯
-
subscribe依赖于回调,但您使用的示例回调没有与useCallback一起记忆。因此,每次渲染您的订阅依赖项都会发生变化,这会导致一些级联。将您的回调包含在useCallback中是否可以解决问题? -
@DennisVash 绝对给我几分钟,但请记住这段代码会阻止浏览器选项卡,这就是我一开始不想把它放在这里的原因。完成后将使用“演示”更新问题
-
我认为这可以解决问题:codesandbox.io/s/dazzling-booth-dngiq?file=/index.js。 (id 是近似的,但不准确)
标签: javascript reactjs