簡體   English   中英

React 無限重新渲染,useEffect 問題,未觸發設置 state

[英]React infinite rerender, useEffect issue, no set state triggered

我的組件似乎進入了無限循環,我找不到原因。 我正在使用useEffectuseState掛鈎,但事實並非如此。 對於任何想要將其標記為重復的人:請仔細閱讀該問題。 這種無休止的重新渲染循環場景僅在以下情況下發生:

  1. 鼠標移動太快
  2. 用戶單擊開發者控制台中的任何內容,然后觸發mousemove事件(為了清楚起見,我省略了mousedown / mouseup處理程序,但它們與mousemove處理程序基本相同,觸發對每個訂閱回調的調用.

這是一個示例,但請注意它可能會導致無限循環並強制您關閉瀏覽器選項卡Codesandbox:https://codesandbox.io/s/infiniteloopissue-lynhw?file=/index.js

我正在使用Map.forEachmousemove事件上觸發任何訂閱者的setState (您可以在下面看到相關代碼),我正在使用useEffect訂閱/取消訂閱此“更新事件”

以下是“解決”問題的一件事 - 檢查參考點注釋(在鼠標使用者下方)。 如果回調作為useCallback掛鈎的依賴項被刪除,則一切正常。 但這並不好,因為在這個例子中,我們顯然只是將數據提取到 state 中,但是callback function 可能依賴於,比如其他一些 state,在這種情況下它不會起作用。 回調需要是可變的。

我的猜測是,反應以某種方式設法在.forEach完成它的迭代之前重新渲染,在這種情況下,它會取消訂閱(從而刪除密鑰),並重新訂閱(從而再次添加它)觸發另一個回調,然后觸發另一個取消訂閱/重新訂閱,我們 go 進入一個循環。 但這應該是不可能的吧? 我的意思是 javascrip 應該阻塞單線程,如何/為什么在 forEach 循環中間重新渲染反應?

此外,是否有人對如何“訂閱” mousemove移動並運行回調有更好的想法。 我最近在一些后端代碼中看到了一些EventEmitter ,但對它並不熟悉。 我也不確定這是否可以解決這里的問題,在更新時反應問題優先於等待主線程完成.forEach循環(至少我認為是這樣)

基礎很簡單:

應用程序.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>
  )
}

該組件背后的想法是,始終在屏幕上記下當前鼠標 position。 此信息可以在上下文中輕松獲得,但是為了在屏幕上呈現它,我們需要觸發“重新渲染”,因此 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 object。 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 實現了兩個函數,將id subscribeunsubscribe到回調的 Map 中,之前在 Provider 中引用過,這就是它所做的一切,它不調用回調,它永遠不會觸發其他任何東西從 Map object 添加/刪除callback 所有的“更新”都是由提供者完成的,或者更確切地說是回調提供者調用的組件,在每個mousemove上。 換句話說, useEffect不會觸發 state 更新,鼠標交互會。

useEffect掛鈎返回一個取消訂閱 function 以確保它“自行清理”,以便在卸載組件時不會觸發callback

問題

好吧,正如我所說的,問題是,我最終會使用這個組件進行無休止的重新渲染循環,並且只有當鼠標移動得太快或者它“離開屏幕”(例如進入開發人員控制台)時才會發生這種情況。

編輯:完全刪除上下文,沒有必要並且造成混亂。 EDIT2:添加了代碼框

拆分subscribeunsubscribe兩個不同useEffect調用:

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

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

這可以防止回調被刪除和重新創建,但如果它卸載了組件,仍然會清理它。 它看起來確實有點笨拙,但它似乎確實有效。

根據反應文檔

清理 function 在從 UI 中刪除組件之前運行,以防止 memory 泄漏。 此外,如果一個組件多次渲染(通常是這樣),則在執行下一個效果之前會清除上一個效果

(例如當效果依賴改變時)

編輯示例沙箱

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM