React Hooks總結

學霸初養成發表於2020-05-04

Hook 前言

什麼是Hook

自從 16.8 版本開始,hooks 的出現使得你可以在不編寫 class 的情況下使用狀態管理以及其它 React 的特性。

那麼在 React Hooks 出現之前,class 類元件和 function 函式元件有什麼區別?Hooks 出現之後,函式元件又是如何滿足原來只有類元件才有的功能的?

1.類元件和沒有 hooks 加持的函式元件:

函式元件常被稱為無狀態元件,意思就是它內部沒有狀態管理,只能做一些展示型的元件或者是完全受控元件。因此差別主要體現在:

  • 函式元件沒有內部狀態管理
  • 函式元件內部沒有生命週期鉤子
  • 函式元件不能被獲取元件例項 ref,函式元件內也不能獲取類元件的 ref

2.類元件和有 hooks 加持的函式元件:

有了 hooks 加持之後,函式元件具備了狀態管理,除了可以使用內建的 hooks ,我們還可以自定義 hooks。

  • 類元件有完備的生命週期鉤子,而函式元件只能具備:DidMount / WillUnmount / DidUpdate / willUpdate
  • 函式元件內部可以通過內建 hook 獲取類元件 ref,也可以通過一些 API 的組合使用達到獲取函式元件 ref 的功能
  • 函式元件具備了針對狀態變數的 setter 監聽(類似於 vue watch),類元件沒有這種 API。(useCallback、useEffect、useMemo等)

類元件原本比函式元件更加完整,為什麼還需要 hooks?

這要說到 React 的設計理論:

  • React 認為,UI 檢視是資料的一種視覺對映,即 UI = F(DATA) ,這裡的 F 需要負責對輸入的資料進行加工、並對資料的變更做出響應
  • 公式裡的 F 在 React 裡抽象成元件,React 是以元件為粒度編排應用的,元件是程式碼複用的最小單元
  • 在設計上,React 採用 props 來接收外部的資料,使用 state 屬性來管理元件自身產生的資料(狀態),而為了實現(執行時)對資料變更做出響應需要,React 採用基於類 Class 的元件設計
  • 除此之外,React 認為元件是有生命週期的,因此開創性地將生命週期的概念引入到了元件設計,從元件的 create 到 destroy 提供了一系列的 API 共開發者使用

類元件 Class Component 的困局

元件狀態邏輯複用困局

對於有狀態元件的複用,React 團隊和社群嘗試過許多方案,早期使用 CreateClass + Mixins,使用 Class Component 後又設計了 Render Props 和 HOC,再到後來的 Hooks設計,React 團隊對於元件複用的探索一直沒有停止。

HOC 和 Render Props 都有自己的缺點,都不是完美的複用方案(詳情瞭解 React HOC 和 Render Props),官方團隊認為應該為共享狀態邏輯提供更好的原生途徑。在 Hooks 加持後,功能相對獨立的部分完全抽離到 hook 實現,例如網路請求、登入狀態、使用者核驗等;也可以將 UI 和功能(狀態)分離,功能放到 hook 實現,例如表單驗證。

複雜元件變得難以理解

我們經常維護一些元件,它們起初很簡單,但是逐漸會被狀態邏輯和副作用充斥。在多數情況下,不可能將元件拆分為更小的粒度,因為狀態邏輯無處不在。這也給測試帶來了挑戰。Hook 可將元件中相互關聯的部分拆分成更小的函式

JavaScript Class 的缺陷
  • this的指向問題(語言缺陷)
  • 編譯後體積和效能的問題

同樣功能的類元件和函式元件,在經過 Webpack 編譯後體積相差明顯,也伴隨著一定的效能問題。這是因為 class 在 JavaScript 中本質是函式,在 React 內部也是當做 Function類 來處理的。而函式元件編譯後就是一個普通的 function,function 對 JS 引擎是友好的。

內建 Hooks

useState

const [state, setState] = useState(initialState);

用來承擔與類元件中的 state 一樣的作用,元件內部的狀態管理

function () {
  const [ count, setCount ] = useState(0);  
  const onClick = ()  => { 
    setCount( count + 1 ); 
    // setCount(count => count + 1);
  }; 
    
  return <div onClick={onClick}>{ count }</div>  
}

除了直接傳入最新的值,還可以函式式更新,這樣可以訪問到先前的 state。如果你的初始 State 建立比較昂貴時,可以傳一個函式給 useState:

function Table(props) {
  // ⚠️ createRows() 每次渲染都會被呼叫
  const [rows, setRows] = useState(createRows(props.count));
  // ...
}

function Table(props) {
  // ✅ createRows() 只會被呼叫一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}

如果是複雜型別的 state,需要傳入修改後的完整的資料,不再像類元件中的 setState 可以自動合併物件,需要手動合併:

setState(prevState => ({...prevState, ...updatedValues}));

此外,useReducer 是另一種可選的方案。

useEffect

useEffect(func, [deps]);

可以用來模擬生命週期,即可以完成某些副作用。什麼叫副作用?一般我們認為一個函式不應該對外部產生影響,一旦在函式內部有某些影響外部的操作,將其稱之為副作用。例如改變 DOM、改變 Window物件(Global)、設定定時器、使用原生API繫結事件等等,如果處理不好,它們可能會產生 bug 併產生破壞。

如果只傳一個引數,每次元件渲染都會執行回撥函式(掛載+跟新),相當於 componentDidMount() + componentDidUpdate()

返回值函式:在元件更新前、元件解除安裝時執行,相當於 componentWillUnmount() + componentWillUpdate()

useEffect(() => { // 每次渲染後執行此函式,獲取到的值是最新的
    console.log("Effect after render", count);
    return () => { // 每次執行useEffect前,先執行此函式,獲取到的資料是更新之前的值
        console.log("remove last", count);
    }
});

第二個引數是依賴列表,當依賴的狀態資料發生改變時會執行回撥

1.如果是一個空陣列,表示沒有依賴項

  • 回撥函式:只在元件掛載的時候執行一次,相當於 componentDidMount()
  • 返回值函式:只在元件解除安裝的時候執行一次,相當於 componentWillUnmount()

2.如果有值

  • 回撥函式:除了具有 componentDidMount(),還當 陣列內的變數發生變化時執行 componentDidUpdate()
  • 返回值函式:除了具有 componentWillUnmount(),還當 陣列內的值發生變化時執行 componentWillUpdate()

需要注意的是,

    1.第二個引數的比較其實是淺比較,傳入引用型別進去是無意義的
    2.一個元件內可以使用多個 useEffect,它們相互之間互不影響
    3.useEffect 第一個引數不能是 async 非同步函式,因為它總是返回一個 Promise,這不是我們想要的。你可以在其內部定義 async 函式並呼叫

useLayoutEffect

它與 useEffect 的用法完全一樣,作用也基本相同,唯一的不同在於執行時機,它會在所有的 DOM 變更之後同步呼叫 effect,可以使用它來

useEffect 不會阻塞瀏覽器的繪製任務,它會在頁面更新之後才執行。而 useLayoutEffect 跟 componentDidMount 和 componentDidUpdate 的執行時機一樣,會阻塞頁面渲染,如果當中有耗時任務的話,頁面就會卡頓。大多數情況下 useEffect 比 class 的生命週期函式效能更好,我們應該優先使用它。

如果你正在將程式碼從 class 元件遷移到使用 Hook 的函式元件,則需要注意 useLayoutEffect 與 componentDidMount、componentDidUpdate 的呼叫階段是一樣的。但是,我們推薦你一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffect。

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案,它接收一個 (state, action) => newState 的 reducer 處理函式,並返回當前的 state 和 配套的 dispatch 方法。使用方法與 redux 非常相似。

某些場景下,useReducer 比 useState 更加適用:

  • 當狀態變數比較複雜且包含多個子值的時候
  • 下一個 state 依賴之前的 state
const initialState = {count: 0};

function init(initialCount) {
  return {count: initialCount};
}

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(props) {
  const [state, dispatch] = useReducer(reducer, initialState);
  // const [state, dispatch] = useReducer(reducer, props.initialCount, init);

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

此外,它還可以模擬 forceUpdate()

const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
   forceUpdate();
}

useCallback

const memoizedCallback = useCallback(func, [deps]);

useCallback 快取了方法的引用。它有的作用:效能優化,父元件更新,傳遞給子元件的函式指標不會每次都改變,只有當依賴項發生改變的時候才會改變指標。避免了子元件的無謂渲染

它的本質是對函式依賴進行分析,依賴變更時才重新執行。

useMemo & React.memo

useMemo 用於快取一些耗時的計算結果(返回值),只有當依賴項改變時才重新進行計算。

useCallback(func, [deps])  等同於  useMemo(() => func, [deps])

useCallback 快取的是方法的引用,useMemo 快取的是方法的返回值,適用場景都是避免不必要的子元件渲染。

在類元件中有 React.PureComponent,與之對應的函式元件可以使用 React.memo,它們都會在自身 re-render 時,對每一個 props 項進行淺對比,如果引用沒有發生改變,就不會觸發渲染。

那麼,useMemo 和 React.memo 有什麼共同點呢?前者可以在元件內部使用,可以擁有比後者更細粒度的依賴控制。它們兩個與 useCallback 的本質一樣,都是進行依賴控制。

useContext

專門為函式元件提供的 context hook API,可以更加方便地獲取 context 的值。

const value = useContext(MyContext);

useContext(MyContext) 接收一個 context 物件,當前獲取到的值由上層元件中距離最近的 <MyContext.Provider> 的 value 決定。

useContext(MyContext) 相當於之前的 static contextType = MyContext 或者 <MyContext.Consumer>

useRef

const refContainer = useRef(initialValue);

useRef 返回一個可變的 ref 物件,其 current 屬性被初始化為傳入的引數。返回的 ref 物件在元件的整個生命週期內保持不變。

注意:此 hook 可以獲取 DOM 元素、類元件示例,但無法獲取函式元件例項,因為函式元件根本沒有例項。如果想讓函式元件被獲取到 ref,可以使用 useImperativeHandle 來達到這樣的效果

另外,useRef 獲取到的“ref”物件是一個 current 屬性可變且可以容納任意值的通用容器。可以實現如下功能:

  • 模擬例項變數
  • 獲取 prevProps、prevState
// 當做 class 例項變數
function Timer() {
  const intervalRef = useRef();
 
  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });
 
  // ...
}
 
// 獲取prevProps,prevState
function Counter(props) {
  const [count, setCount] = useState(0);
  const prevProps = useRef(props);
  const prevCount = useRef(count);

  useEffect(() => {
    prevCount.current = count;
    prevProps.current = props;
  });
 
  return <h1>Now: {count} - {props}, before: {prevCount.current} - {prevProps.current}</h1>;
}

useImperativeHandle

useImperativeHandle 可以讓你在使用 ref 時自定義對外暴露的屬性。官方指出,它應當與 forwardRef 一起使用。

示例:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

此時,通過 ref 獲取到 FancyInput 的"例項",其 current 屬性內只有 foucs 屬性可供訪問

 

相關文章