【问题标题】:React infinite rerender, useEffect issue, no set state triggeredReact 无限重新渲染,useEffect 问题,未触发设置状态
【发布时间】:2020-06-04 16:18:13
【问题描述】:

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

  1. 鼠标移动得太快
  2. 用户单击开发者控制台中的任何内容,然后触发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 实现了两个函数,分别是 subscribeunsubscribeid 到回调的 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


【解决方案1】:

subscribeunsubscribe 拆分为两个不同的useEffect 调用:

useEffect(() => {
    subscribe()
  }, [subscribe])

useEffect(() => unsubscribe, [unsubscribe])

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

根据react docs

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

(例如当效果依赖改变时)

【讨论】:

  • 是的,我不认为清理是一个问题,它正在以任何一种方式执行。每次触发callback 时,组件都会取消订阅,这是毫无疑问的。问题是它也会在.forEach 循环完成之前重新订阅,我知道这是怎么发生的,但这是我看到的唯一合理的解释。将它们分开后,它现在运行良好,尽管正如您所说,有点笨拙。
猜你喜欢
  • 2021-09-14
  • 1970-01-01
  • 2020-09-13
  • 2019-07-31
  • 2023-02-22
  • 1970-01-01
  • 1970-01-01
  • 2020-08-06
  • 1970-01-01
相关资源
最近更新 更多