你不知道的React Hook

前言

自 React16.8 正式發佈React Hook以後,已通過去了5個版本(本博客於React 16.13.1版本時發佈)。本身使用Hook已經有了一段時間,不得不說在最初使用Hook的時候也進入了不少誤區。在這篇文章中,我會拋出並解答我遇到的一些問題,同時整理對Hook的心得理解。react

1. Hook的執行流

在解釋後續內容以前,首先咱們要明確Hook的執行流。git

咱們在寫 Hook Component 的時候,本質是 Function Component + Hook ,Hook只是提供了狀態管理等能力。對 Function Component 來講,每一次渲染都會從上到下將全部內容從新執行一次,若是有變量,就會創造新變量github

來看一個簡單的例子數組

// father.js

import Child from './child';

function Father() {
    const [num, setNum] = useState(0);

  	function handleSetNum() {
        setNum(num+1);
    }
  
    return (
        <div> <div onClick={handleSetNum}>num: {num}</div> <Child num={num}></Child> </div>
    );
}
複製代碼
// child.js

function Child(props) {
    const [childNum, setChildNum] = useState(0);
  
    return (
        <> <div>props.num: {props.num}</div> <div onClick={() => {setChildNum(childNum + 1)}}> childNum: {childNum} </div> </> ); } 複製代碼

而後咱們來看看執行流:瀏覽器

father執行 useStateuseState 返回一個數組,而後解構賦值給 numsetNum ,而後建立函數 handleSetNum ,把 jsx代碼 返回出去交給 react 處理。緩存

child接受到來自father傳遞的數據,建立props變量並賦值,執行 useState ,解構賦值給 childNumsetChildNum ,把 jsx代碼 返回出去交給 react 處理。性能優化


接下來,點擊father中那個綁定了點擊事件的變量,觸發執行 handleSetNum 方法,修改Hook State,讓num值加一。由於狀態發生改變,會去觸發react的重渲染。session

而後咱們再來看看第二次重渲染時的部分執行流:閉包

father會 **再次執行 useState **; useState 返回一個 新數組 ,而後解構賦值給 **新建立的 numsetNum **;隨後 建立一個新的函數 handleSetNum函數

child接受到來自father傳遞的數據,建立 新變量props並賦值;再次執行 useStateuseState返回一個新數組,但由於childNum沒有發生變化,新數組裏面的值全等於舊數組裏面的值,解構賦值給 新變量 childNumsetChildNum


咱們能夠經過全局變量進行驗證

// father.js

window.selfStates = []; // 建立一個全局的變量存儲
window.selfStates2 = []; // 建立一個全局的變量存儲

function Father() {
    const [num, setNum] = useState([0]); // 把數字改爲一個數組
    const [num2, setNum2] = useState([0]); // 設置一個對照組,對照組就初始化,不進行修改
    window.selfStates.push(num);
    window.selfStates2.push(num);
  
  	function handleSetNum() {
        setNum([num[0] + 1]) // 不直接改值,避免影響舊數組
    }

  	...
}
複製代碼

以後能夠在瀏覽器的控制檯裏輸出,看看結果

固然,咱們建立的 handleSetNum 函數也能夠用這樣的方法進行驗證。

因此,看到這裏,咱們已經瞭解了hook的執行流:每次渲染,每次更新,都會讓整個內容所有更新一次,而且建立新的變量。

2. useState不要傳遞執行函數進行初始化

直接上代碼

function initState() {
  	console.log('run'); // 執行一次就知道了
		return 1;
}

function Father() {
  	const [num, setNum] = useState(initState()); // ❎
  	const [num, setNum] = useState(initState); // ✅
}
複製代碼

由於每次更新,都會執行一次useState,根據useState的機制,它會進行數據存儲:若是沒有數據,進行初始化,並建立新數據,存儲起來;若是有數據,不進行初始化操做,返回數據的值。

可是若是咱們使用 useState(func()) 這樣形式進行初始化,那麼每次都會先執行func,再把執行後獲得的值做爲參數傳遞給useState,可是若是已經初始化過了,那麼會跳過對這個參數的初始化處理,每次更新時都會浪費一次跑func函數的時間。

3. Capture value

Hook Component

來看下面這個代碼

function Father() {
    const [num, setNum] = useState(0);

    function handleSetNum() {
        setNum(num+1);
    }

  	// 延遲3秒後輸出num值
    function handleGetNum() {
        setTimeout(() => {
            alert(num);
        }, 3000);
    }

    return (
        <div> <div onClick={handleSetNum}>num: {num}</div> <div onClick={handleGetNum}>點我輸出內容</div> </div>
    );
}
複製代碼

假設如今num值爲0,我先觸發 handleGetNum ,而後再觸發1次 handleSetNum 修改num的值,3秒倒計時結束後,輸出的num值是多少?

留點空間來思考一下













下面公佈答案













答案是0

若是咱們使用 Class Component 來作,獲得的結果卻相反,輸出的值爲1。

在Hook Component中,這種現象被稱做 Capture value


爲何會有這樣的狀況?

解答這個問題,這仍是得回到最初的起點:Hook執行流

咱們知道每次渲染都會建立該次渲染的新變量,所以在最初的狀態下,我用handleGetNum_0來表示最初的 handleGetNum 函數,num_0表示最初的num。

咱們先觸發 handleGetNum,隨後觸發 handleSetNum ,以後數據更新,建立新函數 handleGetNum_1 和 新變量 num_1。

在這個過程當中咱們實際點擊的是handleGetNum_0,handleGetNum_0裏操做的是num_0,因此 alert 的內容仍是 num_0 的值。

Class Component

那麼問題來了,爲何 Class Component 不會有這樣的狀況發生?

咱們來看一下若是用 Class Component 會怎麼寫

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            num: 0
        }
    }

    handleSetNum = () => {
        this.setState({num: this.state.num+1});
    }

    handleGetNum = () => {
        setTimeout(() => {
            alert(this.state.num);
        }, 3000);
    }

    render() {
        return (
            <div> <div onClick={this.handleSetNum}>num: {this.state.num}</div> <div onClick={this.handleGetNum}>點我輸出內容</div> </div>
        )
    }
}
複製代碼

咱們在 Class Component 中獲取參數時,是經過 this.state.num 進行獲取,this會指向最新的state值,所以不會出現 Capture Value的狀況。

若是咱們想在 Class Component 中實現 Capture Value,一個簡單的辦法就是作一個閉包,這個比較簡單,所以不在文中詳細陳述。


那麼若是咱們不想在 Hook Component 中觸發 Capture Value 應該怎麼作?

答案就是用 useRef

function App() {
    const [num, setNum] = useState(0);
    const nowNum = useRef(0); // 額外建立一個Ref對象,由於Ref對象更新不會觸發react的重渲染

    function handleSetNum() {
        setNum(num + 1);
        nowNum.current = num + 1; // 給num賦值的同時也給nowNum賦值
    }

    function handleGetNum() {
        setTimeout(() => {
            alert(nowNum.current);
        }, 3000);
    }
  
  	...
}
複製代碼

咱們使用 nowNum.current 就相似 this.state ,由於Ref會建立一個對象,這個對象會指向最新的值。

4. 須要用useCallback和useMemo嗎

useCallback 和 useMemo 比較相似,咱們放在一塊兒來講。

他們都是用來作數據緩存的,區別就在於useCallback返回的是函數,useMemo返回的是函數執行後的返回值。

先上結論

何時須要用他們:

  1. 成本很高的計算
  2. 避免子組件無心義的重渲染
  3. 數據須要傳遞給其餘組件,且數據爲對象、函數

何時不須要用他們:

  1. 僅僅在組件內部使用,不存在向下傳遞數據。
  2. 若是要向下傳遞數據,但數據值是非對象、非函數的值

  1. 若是我有一個jsx代碼,裏面有一個值須要經過計算得出。這個計算很是複雜,計算成本很高,可是計算時用到的參數基本沒有變化,那麼此時用useMemo進行包裹,就能節省不少計算成本。

  1. 若是有一個父組件,有一個子組件,父組件的每次更新必定會觸發子組件的更新。使用useMemo包裹子組件,能避免子組件的無心義更新。

ex.

function Father() {		
		const [num, setNum] = useState([0]);
    const [num2, setNum2] = useState([0]);

    function handleSetNum() {
        setNum([num[0] + 1]);
    }

    function handleSetNum2() {
        setNum2([num2[0] + 1]);
    }

    return (
        <div> <div onClick={handleSetNum}>num: {num[0]}</div> <div onClick={handleSetNum2}>num2: {num2[0]}</div> <Child num={num}></Child> </div>
    );
}
複製代碼

上面的代碼就是沒有使用useMemo,Child接受num參數,每當num參數更新的時候,會觸發Child的更新,同時還有一個num2,num2的更新觸發Father的更新,同時形成Child的更新。

處理後的代碼:

function Father() {
    const [num, setNum] = useState([0]);
    const [num2, setNum2] = useState([0]);

    function handleSetNum() {
        setNum([num[0] + 1]);
    }

    function handleSetNum2() {
        setNum2([num2[0] + 1]);
    }

    const ChildHtml = useMemo(() => {
      	return <Child num={num}></Child>
    }, [num])

    return (
        <div> <div onClick={handleSetNum}>num: {num[0]}</div> <div onClick={handleSetNum2}>num2: {num2[0]}</div> {ChildHtml} </div>
    );
}
複製代碼

這樣咱們用useMemo包裹了子組件,useMemo會存儲return的值,當num變化時,會從新執行useMemo第一個參數裏的函數,返回新值,並保存新值。 當num2變化時,會把以前保存的值取出來,這樣就能避免子組件的重渲染。

當心對象!

有時候會有這樣的處理

// father.js
function Father() {
    ...
    function func() {};

    return {
      	<Child func={func} obj={{name: abc, num:2}}></Child>
    }
}

// child.js
function Child(props) {
    useEffect(() => {
        ...
    }, [props])

    return (
        ...
    );
}
複製代碼

在子組件裏,須要模擬 componentDidUpdate 處理各類參數,用當props改變的時候,進行一些操做。

若是咱們已經用了useMemo包裹了子組件,那麼解決了一個隱藏問題。若是咱們沒有用useMemo包裹子組件,那麼就要當心了

Father傳遞給Child的props裏有2個屬性,一個函數,一個自定義對象

props: {
    func: func,
    obj: {
      	name: abc,
        num: 2
    }
}
複製代碼

當子組件接受的props發生了變化,會執行useEffect函數裏的內容。

若是咱們props.obj裏的值發生了改變,引發了useEffect的執行,這是正常的。

可是咱們要明確一點,Hook每次執行都會建立新的對象。也就是說,若是Father每次更新,都會建立新的func函數和新的obj對象,即便咱們認爲func函數和obj對象沒有發生變化,可是props裏的變量指針都會指向新對象,而後觸發了本不應觸發的useEffect


2種解決方法:

  1. 用useMemo包裹子組件。
  2. 用useCallback和useMemo包裹func函數和obj對象

也許有人會採用 React.memoReact.PureComponent ,可是他們的緩存策略也只是用全等符比較props裏的值,所以即便使用了這2個方法,若是不用useCallback和useMemo包裹props中傳遞的值,依然也會觸發上文代碼裏寫的 useEffect

tip: 經過useState獲得的變量不須要使用useMemo,由於useState已經進行了處理,保證未更新的值引用不變

const num = useState(0); // 由於返回的是一個數組,每次更新num會變,可是若是具體的state值沒有變化,num[0]和num[1]的引用不會變。

const [num, setNum] = useState(0); // 這種狀況下,若是num的值沒變,每次更新num和setNum的引用就不會變
複製代碼

tip2: 若是傳遞的是非對象、非函數的內容,好比number、string,就不必包裹。

全等符比較他們是比較值是否相等,而不是去比較地址是否相等


這裏只對props傳遞進行了距離,一些非props傳遞的地方,只要用上了對象,都有可能埋下這種隱藏問題。 因此必定要當心對象!


性能優化

還要明確一點,不少時候,從新執行一段代碼(不用useMemo/useCallback)遠比存儲、比較、取值(使用useMemo/useCallback)來得更快。並且在不少狀況,即便用 useMemo/useCallback 進行優化,優化效果也根本看不出來(現代瀏覽器和計算機速度並不慢)

因此若是不是爲了優化多組件嵌套或者高成本計算,不少時候其實也不須要刻意去使用useMemo/useCallback

感興趣的能夠瀏覽 When to useMemo and useCallback 以獲取更多信息

總結

  1. Hook Component 每一次渲染都會從上到下將全部內容從新執行一次,若是有變量,就會創造新變量

  2. useState(func()) 不要這樣寫代碼!

  3. Hook Component 具有 Capture value 的特性

    • 能夠用 useref 避開 Capture value
  4. useCallback 和 useMemo 的注意事項

    何時須要用他們:

    1. 成本很高的計算
    2. 避免子組件無心義的重渲染
    3. 數據須要傳遞給其餘組件,且數據爲對象、函數

    何時不須要用他們:

    1. 僅僅在組件內部使用,不存在向下傳遞數據。
    2. 若是要向下傳遞數據,但數據值是非對象、非函數的值
  5. 必定要當心對象!!

  6. 有時候 從新執行一段代碼遠比從緩存中獲取一段結果來得更快


尾聲

若是文中有錯誤/不足/須要改進/能夠優化的地方,但願能在評論裏友善提出,做者看到後會在第一時間裏處理

若是你喜歡這篇文章,👍點個贊再走吧,github的星星⭐是對做者持續創做的支持❤️️


相關資料

kentcdodds.com/blog/usemem…

overreacted.io/zh-hans/how…

zhuanlan.zhihu.com/p/85969406?…

相關文章
相關標籤/搜索