React Hooks總結


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 屬性可供訪問

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM