Hooks 的性能優化及可能會遇到的坑總結

組件 PureRender

class 組件中性能優化能夠經過 shouldComponentUpdate 實現或者繼承自 PureComponent,固然後者也是經過 shouldComponentUpdate 去作的,內部對 stateprops 進行了 shallowEqual。數組

對於函數組件來講並無這個生命週期能夠調用,所以想實現性能優化只能經過 React.memo(<Component />) 來作,這種作法和繼承 PureComponent 的原理一致。性能優化

另外若是你的函數組件須要拿到它的 ref,可使用如下工具函數:微信

function memoForwardRef<N, P>(comp: RefForwardingComponent<N, P>) {
  return memo(forwardRef<N, P>(comp));
}
複製代碼

可是並非以上作法之後性能就萬事大吉了,你還得保證傳遞的 props 以及內部的狀態的引用不發生預期以外的變化。閉包

保持局部不變

對於函數組件來講,變量的引用是須要重點關注的問題,不管是函數亦或者對象。函數

const Child = React.memo(({ columns }) => {
  return <Table columns={columns} />
})
const Parent = () => {
  const data = [];
  return <Child columns={data} />
}
複製代碼

對於以上組件來講,每次 Parent 渲染的時候雖然 columns 內容沒有變,可是 columns 的引用已經變了。當 props 傳遞給 Child 的時候,即便使用了 React.memo 可是性能優化也失效了。工具

對於這種狀況,能夠經過 useMemo 將引用存儲起來,依賴不變引用也就不變。性能

const data = useMemo(() => [], [])
複製代碼

useMemo 的場景可能是用於值的計算。好比密集型計算場景下你確定不但願組件從新渲染的時候,依賴項沒有變動缺重複執行計算函數獲得相同的值。優化

對於函數來講,若是你想保存它的引用的話可使用 useCallback 來作。ui

function Counter() {
  const [count, setCount] = useState(0)


  // 這樣寫函數,每次從新渲染都會再次建立一個新的函數
  const onIncrement = () => {
    setCount(count => count + 1)
  }

  const onIncrement = useCallback(() => {
    setCount(count => count + 1)
  }, [])

  return (
    <div> <button onClick={onIncrement}>INCREMENT</button> <p>{count}</p> </div>
  )
}
複製代碼

對於以上代碼來講,組件每次渲染的時候使用了 useCallback 包裹的 onIncrement 函數引用不會改變,這也就意味着不須要頻繁建立及銷燬函數了。spa

可是在 useCallback 存在依賴的狀況下函數引用並不必定按照你的想法正常保持不變,好比以下案例:

function Counter() {
  const [count, setCount] = useState(0)

  const onIncrement = useCallback(() => {
    setCount(count => count + 1)
  }, [])
  
  const onLog = useCallback(() => {
    console.log(count)
  }, [count])

  return (
    <div> <button onClick={onIncrement}>INCREMENT</button> <button onClick={onLog}>Log</button> <p>{count}</p> </div>
  )
}
複製代碼

count 每次改變形成組件從新渲染的時候,onLog 函數都會從新建立一次。兩種常規方法能夠保持在這種狀況下函數引用不被改變。

  1. 使用 useEventCallback
  2. 使用 useReducer
function useEventCallback(fn, dependencies) {
  const ref = useRef(() => {
    throw new Error('Cannot call an event handler while rendering.');
  });

  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}
複製代碼

useEventCallback 使用了 ref 不變的特性,保證回調函數的引用永遠不變。另外在 Hooks 中,dispatch 也是不變的,因此把依賴 ref 改爲 dispatch,而後在回調中調用 dispatch 就是另外一種作法了。

性能優化並非銀彈

凡事都有兩面性,在引入以上這些性能優化的時候你已經下降了本來的性能,畢竟它們都是有使用代價的,咱們能夠來閱讀下 useCallbackuseMemo 的核心源碼:

function updateCallback(callback, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

function updateMemo(nextCreate, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
複製代碼

上述源碼實現思路大體是從 fiber 中取出 memoizedState,而後對比先後 Deps,對比的實現也採用了 shallowEqual,最後若是有變化的話就重置 memoizedState

能夠看出來,本文中講到的性能優化方案基本都是採用了 shallowEqual 來對比先後差別,因此不必爲了性能優化而優化。

Hooks 的坑

Hooks 的坑 99% 都是閉包引發的,咱們經過一個例子來了解下什麼狀況下會由於閉包致使問題。

function App() {
  const [state, setState] = React.useState(0)
  // 連點三次你以爲答案會是什麼?
  const handleClick = () => {
    setState(state + 1)
    setTimeout(() => {
      console.log(state)
    }, 2000)
  }

  return (
    <>
      <div>{state}</div>
      <button onClick={handleClick} />
    </>
  )
}
複製代碼

上述代碼觸發三次 handleClick 後你以爲答案會是什麼?可能答案與你所想的不大同樣,結果是:

0 1 2

由於每次 render 都有一份新的狀態,所以上述代碼中的 setTimeout 使用產生了一個閉包,捕獲了每次 render 後的 state,也就致使了輸出了 0、一、2。

若是你但願輸出的內容是最新的 state 的話,能夠經過 useRef 來保存 state。前文講過 ref 在組件中只存在一份,不管什麼時候使用它的引用都不會產生變化,所以能夠來解決閉包引起的問題。

function App() {
  const [state, setState] = React.useState(0)
  // 用 ref 存一下
  const currentState = React.useRef(state)
  // 每次渲染後更新下值
  useEffect(() => {
    currentState.current = state
  })

  const handleClick = () => {
    setState(state + 1)
    // 這樣定時器裏經過 ref 拿到最新值
    setTimeout(() => {
      console.log(currentState.current)
    }, 2000)
  }

  return (
    <>
      <div>{state}</div>
      <button onClick={handleClick} />
    </>
  )
}
複製代碼

其實閉包引起的問題多半是保存了 old 的值,只要想辦法拿到最新的值其實基本上就解決問題了。

寫在最後

若是你以爲我有遺漏什麼或者寫的不對的,歡迎指出。

我很想聽聽你的想法,謝謝閱讀。

微信掃碼關注公衆號,訂閱更多精彩內容 加筆者微信羣聊技術
相關文章
相關標籤/搜索