收集的一些React hooks的效能優化以及閉包陷阱問題

一米八的蘿蔔發表於2019-10-28

這篇文章是之前在團隊裡分享的,今天看了一篇hooks的文章才想起分享到掘金,因為寫的很粗糙。有任何問題或者錯誤之處希望可以指出修改,謝謝啦

目錄

  • memo useMemo useCallback 區別
  • useCallback或者useEffect依賴項問題
  • react hooks的閉包陷阱(和依賴項問題可以合併講)
  • useEffect和useLayoutEffect區別

memo useMemo useCallback

  • memo快取元件
import * as React from "react";
import { memo } from "react";
interface IProps {
  count: number;
  handleClick: () => void;
}

const Child: React.FC<IProps> = ({ count, handleClick }) => {
  console.log("Child render");
  return <div onClick={handleClick}>{count}</div>;
};
// 通過淺比較props的形式 props相同的情況下不會觸發重新渲染
export default memo(Child);
複製程式碼
  • useMemo快取值(類似於vue中的computed屬性)
function doubleFn() {
    return count * 2;
  }
  const clickFn = handleClick;
  // 只要count不發生變化 每次重渲染直接返回上一次count*2的計算值,不會觸發handleClick的重新執行
  const doubleCount = useMemo(doubleFn, [count]);
複製程式碼

把“建立”函式和依賴項陣列作為引數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算

  • useCallback快取函式
function handleClick() {
    console.log(`handleCLick-count${count}`);
  }
  // 只要count不發生變化 每次返回都是上一次函式的引用
  const clickFn = useCallback(handleClick, [count]);
  ...
  return (
    // 常用來解決傳入函式的引用每一次都不同而造成重渲染的效能問題
    <Child count={doubleCount} handleClick={clickFn} />
  )
複製程式碼

把內聯回撥函式及依賴項陣列作為引數傳入 useCallback,它將返回該回撥函式的 memoized 版本,該回撥函式僅在某個依賴項改變時才會更新。當你把回撥函式傳遞給經過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子元件時,它將非常有用

總結:三者都是用來做效能優化的

例子

react hoos閉包陷阱

// 第一次渲染
function Counter() {
  const count = 0; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

// 再次點選觸發重新渲染
function Counter() {
  const count = 1; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}

// 再次點選觸發重新渲染
function Counter() {
  const count = 2; // Returned by useState()
  // ...
  <p>You clicked {count} times</p>
  // ...
}
複製程式碼

當我們更新狀態的時候,React會重新渲染元件。每一次渲染都能拿到獨立的count 狀態,這個狀態值是函式中的一個常量。每一次呼叫引起的渲染中,它包含的count值獨立於其他渲染(閉包)

  • 每一次 render 都會生成一個閉包,每個閉包都有自己的 state 和 props(所以在非同步函式中訪問hooks的state值拿到的是當前的閉包值並不是最新的state值).
  • class 中可以用閉包模擬 hooks 的表現。hooks 中可以使用 ref 模擬 class 的表現(例項屬性)。
// class模擬hooks的閉包變現用一個變數儲存當前值
    const count = this.state.count;
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
複製程式碼

解決辦法

  • 新增count依賴 (效能問題:重複建立銷燬定時器)
useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
    // 新增count依賴 缺點會重複生成銷燬定時器影響效能
  }, [count]);
複製程式碼
  • 換成updater方法 count => count + 1
useEffect(() => {
    const id = setInterval(() => {
      // set函式可以為一個函式(類似於setState) 引數為上一次的state值
      setCount(count => count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
複製程式碼
  • useReducer(ps dipatch也不用新增到依賴中)
const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
複製程式碼
  • useRef

依賴項問題

  • 對於一些不依賴hook state變數的函式 可以提取到最外層作用域中(加不加都可以)
  • 對於依賴到hooks變數的 可以用useCallback包一層 新增依賴 或者把函式放進去useEffect中 然後在useEffect中新增依賴

useEffect和useLayoutEffect區別

首先問個問題 useEffect執行時機是什麼時候

渲染完成之後,其實useEffect是非同步的,在渲染完成之後先解綁在重新呼叫effect回撥

useEffect.png

例子

const intervalId = setInterval(() => {
  setLapse(Date.now() - startTime)
  // 如果時間設定較大時看不出問題,但是當設定足夠小0毫秒時就會有問題了
}, 0)
複製程式碼

先點選start 在直接點選clear會大概率發現時間並沒有清零(眼尖的人可以見到先是變成0後有變成時間數,在window下較為明顯)

TIM截圖20191008185906.png

原因

setInterval(fn,0)可以理解為在每一次巨集任務中都插入fn,因為useEffect是在render之後非同步執行的,所以在呼叫clearInterval前就已經插入了 setLapse(Date.now() - startTime) 這麼一段邏輯

執行流程

  • (setRunning(false) setLapse(0))
  • render渲染
  • setLapse(Date.now() - startTime) 罪魁禍首
  • clearInterval
  • useEffect
  • render渲染

造成在render渲染之後又setLapse當前的時間值,最後又多渲染了一次.

解決辦法

那麼知道了罪魁禍首是誰,那麼解決辦法有兩個

  • 不讓setLapse(Date.now() - startTime) 這一段執行 (在interval回撥函式內層加上if(running)是否可行)
const intervalId = setInterval(() => {
  // 加上這個判斷如果為false狀態 那麼也不會執行下面的setLapse(Date.now() - startTime)
  if(running) {
    // 然而並不可行 因為hooks的閉包問題會導致 在當前定時器中running狀態一直為true的
    setLapse(Date.now() - startTime)
  }
}, 0)
複製程式碼
  • setLapse(Date.now() - startTime) 之前就清除定時器

useLayoutEffect

這時候就需要用到useLayoutEffect了 useLayoutEffect會在渲染完整之後同步執行

執行流程

  • (setRunning(false) setLapse(0))
  • render渲染
  • clearInterval
  • useEffect

參考資料

相關文章