你會用React Hooks嗎?

簡介

本文主要針對React `16.8.x`提供的`hooks`使用加以介紹,更高版本的中的`hooks`暫無介紹

優勢

  • 代碼量少(最直觀的體現)
  • 相較於 類組件 使用HOCrender props , hooks 更爲簡單方便的複用狀態組件——狀態處理邏輯複用(後面詳細介紹
  • 100%向後兼容,與 類組件 可同時使用
  • 不需要考慮 this 相關的問題
  • 相較於 , 函數 更容易被機器理解
  • 相較於通過生命週期分割代碼,不相干的邏輯放在同一個生命週期函數中,通過功能區分,放在不同的函數中,代碼更容易理解和維護

劣勢

  • 使用不當性能問題可能會比 類組件 要嚴重
  • 不能使用 decorator (裝飾器)

官方API

useState

這個hooks應該是用的最多的了,

useState 可接受一個參數爲,任意類型數值 或者 可以返回任意類型數值的函數

const [count, setCount] = useState(initialCount);
const [count, setCount] = useState(() => {
    //do something
    return resultCount
})

然後返回一個數組,數組第一個值爲當前組件最新的 state ,只能通過數組的第二個值來更新,第二個值爲更新 state 的工具函數(該工具函數可以接受一個 作爲參數,爲更新後 state 的結果;也可以接受一個 函數 作爲參數,函數的參數爲 state 的前一個值,如果返回值與當前 state 相同則不會重新渲染組件)

function Demo({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      {count}
      <button onClick={() => setCount(0)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}

tip

  • 類組件可以直接對this.state.something賦值(雖然不會更新視圖),但在hooks 組件中不能直接對state賦值
  • hooks 組件state的更新函數同類組件setState一樣是異步的,想要立即獲取更新後的狀態,可以使用useRef
  • useState不會同類組件setState一樣是合併更新,而是直接覆蓋更新
  • useState是基於useReducer的,建議:簡單的數據結構,不同的狀態放在不同的useState中,複雜的數據結構直接使用useReducer
  • useState參數爲函數時,函數只會在初始化是執行一次
const formate = (data) => {
    // do some complicated things
    return result;
}

const [state, setState] = useState(formate(props.data)); // bad; formate每次渲染都要執行

const [state, setState] = useState(() => formate(props.data)); // good

useReducer

const [state, dispatch] = useReducer(reducer, initialState, initialAction);

useReducer 使用方式類似 redux ,適用於複雜狀態的存儲,同步更新狀態,以及深層次更新組件狀態。支持三個參數,第一個參數爲 reducer 函數,第二個參數爲初始狀態initialState , 第三參數爲一個函數(可選),用於對 initialState 初始化處理

const reducerFun = (state, action) => {
    switch (action.type) {
        case 'add':
            return {count: state.count + 1}
        case 'reduce':
            return {count: state.count + 1}
        default:
            return state
    }
}

const initialState = {count: 0};

const initialAction = (init) => {
    return {
        count: init.count + 1
    }
}

function Demo() {
   const [state, dispatch] = useReducer(reducerFun, initialState, initialAction)
    
    return (
        <div>
            {state.count}
            <button onClick={() => dispatch({type: 'add'})}>
                +
            </button>
            <button onClick={() => dispatch({type: 'reduce'})}>
                -
            </button>
        </div>
        
    )
}

tip

  • 深層次更新組件狀態,可以將 dispatch 作爲 props 傳給子組件用於狀態更新
  • 使用 useState 獲取的 setState 方法更新數據時是異步的;而使用 useReducer 獲取的 dispatch 方法更新數據是同步的。

useEffect

useEffect(didUpdate, deps);

useEffect 有支持兩個參數,第一個參數爲 effect (副作用)函數,每次render之後執行,,這個函數可以有返回值,倘若有返回值,返回值也必須是一個函數,姑且稱它爲 清除函數 ,會在組件被銷燬時執行(這句話是片面的)。其實不單是在組件銷燬時執行,當組件更新時,清除函數 的函數的執行時機會被放置到,新一次組件 render 之後執行,然後再執行 effect 函數中非 清除函數部分

如以下demo:

function Demo() {
    const [count, setCount] = useState(0)
    const [random, setRandom] = useState(0)
   
    return (
        <div>
            {count % 2 == 0 && <Child count={count} random={random}/>}
            <button onClick={() => setCount(s => s+1)}> + </button>
            <button onClick={() => setRandom(Math.random())}>random</button>
        </div>
        
    )
}

function Child(props) {
    useEffect(() => {
        console.log('mounted')
        return () => {console.log('unmount')}
    })

    console.log('render')
    return (<h1>{props.count} {props.random}</h1>)
}

在這裏插入圖片描述

可以看出當 Child 組件銷燬時,執行了 清除函數Child 組件創建時,先 render 然後執行了 effect 函數。但是更新 Child 組件時先 render 然後執行了 清除函數 ,然後纔是 effect 函數

第二個參數可選,爲一個數組,組件重新渲染之後,當數組中有值發生了改變時便會執行副作用函數,否則不執行。

function Demo() {
   const [count, setCount] = useState(0)
   const [count1, setCount1] = useState(0)

   useEffect(() => {
       console.log('count') // 只有 count發生改變時纔會打印,count1改變不會
   }, [count])
    
    return (
        <div>
            {count}
            <button onClick={() => setCount((s) => s + 1)}>
                count+
            </button>
            <button onClick={() => setCount1((s) => s + 1)}>
                count1+
            </button>
        </div>
        
    )
}

如果想要只在組件初始掛載或者卸載時執行副作用只需將第二個參數置爲 []

function Demo() {
    useEffect(() => {
        console.log('只有初始掛載或者卸載時執行')
    }, [])

    return (
        <div> </div>
    )
}

tip

  • effect 函數的執行時機是在組件創建或更新 render 之後,如果想要在 render 時同步觸發副作用可以使用 useLayoutEffect
  • 不同於 class組件 在各生命週期中的各種副作用的處理,建議將不同的副作用放置不同的 useEffect 中通過功能進行區分,便於閱讀理解

useLayoutEffect

useLayoutEffect 的使用形式和 useEffect 是相同的,它們之間的唯一區別是: useLayoutEffect 會在所有 DOM 變更之後同步調用。於是,可以使用它來讀取 DOM 佈局並同步觸發重渲染。在瀏覽器執行繪製之前 useLayoutEffect 內部的更新計劃將被同步刷新。與 componentDidMountcomponentDidUpdate 的調用時機是一樣的。因爲是同步調用,可能會產生視覺更新阻塞的問題,所以儘可能使用標準的 useEffect 以避免阻塞視覺更新

tip

  • 執行 DOM 更新操作時 useLayoutEffect 會比 useEffect 更適合使用
  • 涉及使用逐幀動畫 requestAnimationFrame 時,注意執行時機:useLayoutEffect > requestAnimationFrame > useEffect

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(), deps);

useMemouseEffect 兩者語法同樣是一致的,兩者區別是 useEffect 執行的是副作用,一定是在渲染後執行的,useMemo 是需要返回值的,返回值直接參與渲染,因此 useMemo 是在渲染時執行的

先舉個反例:

function Demo(props) {
    const [count, setCount] = useState(1);
    const calculate = (count) => {
        let num = null;
        // num = 炒雞複雜的處理邏輯
        return num;
    }
    return (
        <div count={calculate(count)}>
        </div>
    )
}

Demo組件每次渲染的時候都會執行calculate這個炒雞複雜的計算過程,而參數count偏偏沒有改變,每次重複的計算,是不是很浪費?這時候就可以使用useMemo來優化了。
如下代碼,

function Demo(props) {
    const [count, setCount] = useState(1);
    const number = useMemo(() => {
        let num = null;
        // num = 超級複雜的處理邏輯
        return num;
    }, [count])
    return (
        <div count={number}>
        </div>
    )
}

只有在count發生了改變纔會重新來執行這個炒雞複雜的計算,沒有改變時就直接拿過來之前的計算結果來用,是不是很方便?

tip

  • 注意 deps 參數的設置,避免更新錯誤
  • 傳入 useMemo 的函數會在渲染期間執行。請不要在函數內部執行與渲染無關的操作,諸如副作用這類的操作屬於 useEffect 的適用範疇,而不是 useMemo
  • useMemo的另一常規用法就是和Rect.memo搭配使用減少子組件重複渲染,後文會有詳細介紹

useCallback

const memoizedCallback = useCallback(() => {/*do something*/}, deps);

useCallback 用法和用途與 useMemo 類似,同樣支持兩個參數,第一參數爲要緩存的函數,第二個爲判斷是否更新的依賴數組,其主要區別在於 useCallback 返回值爲函數只能用於緩存函數,useMemo 可以用於緩存值和函數。可用於緩存事件回調函數。

同樣先舉個反例:

function Demo(props) {
    const clickHandle = () => {
        let num = null;
        // num = 超級複雜的處理邏輯
        return num;
    }
    return (
        <div onClick={clickHandle}>
        </div>
    )
}

每次組件更新的時候 clickHandle 都需要重新定義,顯然是沒有必要的,因此可以引入useCallback:

function Demo(props) {
    const clickHandle = useCallback(() => {
        let num = null;
        // num = 超級複雜的處理邏輯
        return num;
    }, [])
    return (
        <div onClick={clickHandle}>
        </div>
    )
}

tip

  • 我們可以認爲:useCallback(fn, deps) 等同於 useMemo(() => fn, deps)
  • useCallback的另一常規用法就是和Rect.memo搭配使用減少子組件重複渲染,後文會有詳細介紹

useRef

const refContainer = useRef(initialValue);

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳遞的參數(initialValue)。

function Demo(props) {
    const box = useRef(null);
    return (
        <div onClick={clickHandle} ref={box}>
        </div>
    )
}

不同於類組件中的ref,它不僅僅適用了DOM元素和類組件的引用,在hooks 組件中可以使用其保存任何值,且在組件的整個生命週期內保持不變。可以直接修改.current 屬性的值且不觸發組件更新。

function Demo(props) {
    const num = useRef(0);
    const [evenNum, setEvenNum] = useState(0)

    const add = () => {
        num.current += 1;
        if(!(num.current & 1)) {
            setEvenNum(num.current)
        }
    }

    return (
        <h1 onClick={add}>
        {evenNum}
        </h1>
    )
}

在這裏插入圖片描述
如圖,當單次數點擊時數字不會更新,偶次數點擊時數字更新


useImperativeHandle

useImperativeHandle(ref, () => ({}))

類組件中想要在父組件中想要獲取子組件實例,只需通過ref屬性直接獲取,然後就可以調用子組件屬性方法,而useImperativeHandle就提供了在hooks 組件中實現該功能的方法;

function Demo() {
    const child = useRef(null);
   
    return (
        <div>
            <Child ref={child}/>
            <button onClick={() => {child.current.add()}}>add</button>
        </div>
        
    )
}

function Child(props, ref) {
    const [count, setCount] = useState(0);

    useImperativeHandle(ref, () => ({
        add: () => setCount((s) => s + 1)
    }))

    return (
        <h1>
            {count}
        </h1>
    )
}

Child = React.forwardRef(Child)

useContext

const value = useContext(MyContext);

useContext接收一個 context 對象(React.createContext 的返回值)並返回該 context 的當前值,能夠在hooks 組件中讀取 context 的值以及訂閱 context 的變化。

const MyContext = React.createContext('test')

function Demo() {
    const [count, setCount] = useState(0)
    return (
        <div>
            <MyContext.Provider value={count}>
                <Child/>
            </MyContext.Provider>
            <button onClick={() => setCount(s => s + 1)}>+</button>
        </div>
    )
}

function Child(props) {
    const myContextValue = useContext(MyContext)
    return (
        <h1>
            {myContextValue}
        </h1>
    )
}

tip

  • 當組件上層最近的 <MyContext.Provider> 更新時,該 Hook觸發重渲染,並使用最新傳遞給MyContext providercontext value 值。即使祖先使用 React.memoshouldComponentUpdate,也會在組件本身使用 useContext 時重新渲染。
  • useContext(MyContext) 相當於 static contextType = MyContext 在類中,或者 <MyContext.Consumer>
  • 搭配 useReducer 可以實現簡易版 redux

memo

memo 不屬於 hooks, 卻是爲 hooks 組件 量身定做的一個 API ,是一個 HOC ,只能用於 函數組件 不能用於 類組件

    function MyComponent(props) {}
    function areEqual(prevProps, nextProps) {
        /*
        如果把 nextProps 傳入 render 方法的返回結果與
        將 prevProps 傳入 render 方法的返回結果一致則返回 true,
        否則返回 false
        */
    }
    export default React.memo(MyComponent, areEqual);

類似 類組件React.PureComponentshouldComponentUpdate(),對傳入組件的 props 進行淺對比或者自定義對比,來決定組件是否需要重新渲染,已達到性能優化的目的。

首先看一個demo:

const MyContext = React.createContext('test')

function Demo() {
    const [count, setCount] = useState(0);
    const [contextNum, setContextNum] = useState(0);
    const [childNum, setChildNum] = useState(0);

    const addChildNum = () => {
        setChildNum(s => s + 1)
    }

    const formateChildNum = (n) => ({value: n + 's'});

    return (
        <div>
            <h1>
                fatherNum: {count}
            </h1>
            <MyContext.Provider value={contextNum}>
                <Child childNum={formateChildNum(childNum)} addChildNum={addChildNum}/>
            </MyContext.Provider>
            <button onClick={() => setCount(s => s + 1)}>add fatherNum</button>
            <button onClick={() => setContextNum(s => s + 1)}>add contextNum</button>
        </div>
    )
}

function Child({childNum, addChildNum}) {
    const myContextNum = useContext(MyContext)
    console.log('render Child')
    return (
        <div>
            <h1>
                myContextNum: {myContextNum}
            </h1>
            <h1>
                childNum: {childNum.value}
            </h1>
            <button onClick={addChildNum}>add childNum</button>
        </div>
        
    )
}

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-bODXNkjM-1589288472736)(./source/optimize1.gif)]
可以看出當父組件中fatherNum改變組件更新時Child組件也隨之重新渲染了,儘管渲染前後並無變化,這顯然是一次無意義的渲染。這只是一個簡單的demo,父組件只有一個state改變,子組件只是多了一次渲染,或許無關痛癢,但是如果父組件stateprops比較多改變比較頻繁,而子組件又十分的‘複雜’,卻額外多餘渲染 N 次,就在用戶體驗上就很可能帶來很差的影響了。

首先我們引入memo,再次只改變fatherNum:

+ Child = memo(Child) 

function Child({childNum, addChildNum}) {
    const myContextNum = useContext(MyContext)
    console.log('render Child')
    return (
        <div>
            <h1>
                myContextNum: {myContextNum}
            </h1>
            <h1>
                childNum: {childNum.value}
            </h1>
            <button onClick={addChildNum}>add childNum</button>
        </div>
        
    )
}

在這裏插入圖片描述
可以看到,引入 memo 後改變 fatherNum,父組件重新渲染以後,子組件仍然觸發了無意義的重渲染。和預想的並不一樣。原因在於:雖然傳給 Child 組件 props 的值沒有什麼改變,但是,由於每次hooks 組件重新渲染以後,組件函數會重新執行一遍,因此 propschildNum 變成了一個新的object 對象,同樣 addChildNum 函數也被重新定義了;(內存空間變了)。這時就需要用到 useMemouseCallback了。

優化後

function Demo() {
    const [fatherNum, setFatherNum] = useState(0);
    const [contextNum, setContextNum] = useState(0);
    const [childNum, setChildNum] = useState(0);
    
    // const addChildNum = () => {
    //    setChildNum(s => s + 1)
    // }
    const addChildNum = useCallback(() => {
        setChildNum(s => s + 1)
    },[]);

    // const formateChildNum = (n) => ({value: n + 's'});

    const resultChildNum = useMemo(() => ({value: childNum + 's'}), [childNum]);

    return (
        <div>
            <h1>
                fatherNum: {fatherNum}
            </h1>
            <MyContext.Provider value={contextNum}>
                <Child childNum={resultChildNum} addChildNum={addChildNum}/>
            </MyContext.Provider>
            <button onClick={() => setFatherNum(s => s + 1)}>add fatherNum</button>
            <button onClick={() => setContextNum(s => s + 1)}>add contextNum</button>
        </div>
    )
}

在這裏插入圖片描述

自定義hooks

自定義hooks本質也是函數,不同於一般函數的是,其內部可以使用useStateAPI做狀態管理。正是因爲有了它存在,纔有了hooks 組件 相對於 類組件 最大的優勢 ———— 狀態處理邏輯複用自定義hooks的存在允許我們將 UI組件無UI組件 相分離。
栗子

https://github.com/streamich/react-use 一個不錯的自定義hooks庫,除了可以拿來用以外還可以參考源碼,總結自己的經驗也是很不錯的。

tip

  • 自定義hooks共享複用的是狀態處理邏輯,是邏輯,而不是單純的狀態本身,每 hooks 都是獨立的

注意事項

  • 不要從常規 JavaScript 函數調用 hooks;
  • 不要在循環,條件或嵌套函數中調用 hooks;
  • 必須在組件的頂層調用 hooks;
  • 可以從 React 功能組件調用 hooks;
  • 可以從自定義 hooks 中調用 hooks;
  • 自定義 hooks 必須使用 use 開頭,這是一種約定;
  • 建議將不需要propsstate的函數提到組件外部。組件每次都會重新渲染,函數每次重新定義,因此一些不必要的函數可以提到函數外面;

最後

本文是對 作者根據自己的經驗以及網上收集到的資料,對 hooks 使用以及注意事項的介紹,可以滿足日常開發使用的需要。如果想要詳細瞭解 其內部的實現機制,與其去看他人消化後的產物,不如直接閱讀源碼:https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.new.js

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章