React 无限重新渲染,useEffect 问题,未触发设置状态

Del*_*ium 6 javascript reactjs

我的组件似乎进入了无限循环,我找不到原因。我正在使用useEffect和useState挂钩,但事实并非如此。对于想要将其标记为重复的任何人:请仔细阅读问题。这种无休止的重新渲染循环场景仅在以下情况下发生:

  1. 鼠标移动太快
  2. 用户单击开发人员控制台内的任何内容,然后mousemove在之后触发事件(为了清楚起见,我省略了mousedown/mouseup处理程序,但它们本质上与mousemove处理程序相同,触发对每个订阅的回调的调用。

这是一个示例,但请注意它可能会导致无限循环并强制您关闭浏览器选项卡 Codesandbox: https ://codesandbox.io/s/infiniteloopissue-lynhw ? file =/ index.js

我正在使用在事件上Map.forEach触发setState任何订阅者的mousemove。(您可以在下面看到相关代码),我正在使用useEffect此“更新事件”订阅/取消订阅

“修复”问题的一件事如下 - 检查参考点评论(在鼠标使用者下方)。如果回调作为useCallback钩子的依赖项被删除,则一切正常。但这并不好,因为在这个例子中,我们显然只是将数据提取到状态中,但该callback函数可能依赖于其他状态,在这种情况下它不起作用。回调需要是可变的。

我的猜测是反应以某种方式设法在.forEach完成它的迭代之前重新渲染,在这种情况下它会取消订阅(从而删除密钥),然后重新订阅(从而再次添加它)触发另一个回调,然后触发另一个回调取消订阅/重新订阅,我们进入一个循环。但这应该不可能吧?我的意思是 javascript 假设阻塞单线程,如何/为什么在 forEach 循环中间重新渲染?

另外,是否有人对如何“订阅” amousemove并运行回调有更好的想法。最近EventEmitter在一些后端代码中看到了一些,但不是很熟悉。我也不确定这是否可以解决这里的问题,更新时反应的问题优先于等待主线程完成.forEach循环(至少我认为是这样)

基础很简单:

应用程序.js

const App = () => {
  return (
    <MouseProvider>
        <Component />
    </MouseProvider>
  )
}
Run Code Online (Sandbox Code Playgroud)

组件.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>
  )
}
Run Code Online (Sandbox Code Playgroud)

该组件背后的想法是,始终在屏幕上写下当前的鼠标位置。这些信息在上下文中很容易获得,但是为了在屏幕上呈现它,我们需要触发“ReRender”,所以 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>
  )
}
Run Code Online (Sandbox Code Playgroud)

因此,每次用户移动鼠标时,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])
}
Run Code Online (Sandbox Code Playgroud)

正如我们所看到的,消费者钩实现两个功能,subscribe并unsubscribe在id进入地图回调,以前在提供参考,也就是它的作用,它不调用回调函数,它永远不会触发任何其他然后添加/删除在callback从Map对象。所有的“更新”都是由提供者完成的,或者更确切地说是回调提供者调用的组件,在每个mousemove. 换句话说,useEffect鼠标交互不会触发状态更新。

该useEffect钩子返回一个取消订阅函数,以确保它“自行清理”,以便callback在组件被卸载时不会被触发。

问题

正如我所说的问题是,我最终会使用此组件进行无休止的重新渲染循环,并且仅在鼠标移动太快或“屏幕外”(例如进入开发人员控制台)时才会发生这种情况。

编辑:完全删除了上下文,没有必要并且导致混乱。EDIT2:添加代码和框

Gar*_*ner 3

subscribe将和拆分unsubscribe为两个不同的useEffect调用:

useEffect(() => {
    subscribe()
  }, [subscribe])
Run Code Online (Sandbox Code Playgroud)

和

useEffect(() => unsubscribe, [unsubscribe])
Run Code Online (Sandbox Code Playgroud)

这可以防止回调被删除和重新创建,但如果卸载了组件,仍然会清理。这看起来确实有点笨拙,但似乎确实有效。

根据反应文档:

清理函数会在组件从 UI 中删除之前运行,以防止内存泄漏。此外,如果一个组件渲染多次(通常会这样做),则在执行下一个效果之前会清除前一个效果。

(例如,当效果依赖性发生变化时)

编辑示例沙箱