useState原理以及其它react-hook的使用

前言

自react16.8發布了正式版hook用法以來,我們公司組件的寫法逐漸由class向函數式組件+hook的方向轉移,雖然用了這么久的hook,但是用得多的基本就useStateuseEffectuseMemo,其他的官方hook因為使用場景不明導致基本沒用過,所以這兩天特地去了解了一下其他hook的使用場景以及useState的原理,然后用這篇文章記錄一下。

useState的使用及其原理

在hook版本出來之前,react函數組件無法擁有自身內部的狀態,而useState賦予了函數組件擁有內部狀態的能力,并且它的使用非常簡單。

  • useState用法
    useState是一個函數,它接收一個初始值,并返回一個數組,該數組的第一位是一個state,第二位則是改變這個的state的函數,比如下面這個計數器的例子,當我點擊按鈕+的時候,數字就+1:

    image.png

    上例中的n就是這個函數組件的內部狀態了。

  • useState原理
    在上面的例子中,我們每次點擊按鈕+的時候,數字都會增加1,也就是說App這個函數會被重新執行一次:

    image.png

    image.png

    既然App會被重新執行,那么useState(0)也會被重新執行一次,但是為什么n的值不會被重置為0呢?
    原因是,第二次useState執行后返回的n并非之前的n,setN改變的并不是之前返回出來的那個n,setN改變的數值存儲于其它地方而非n,之后useState通過閉包的形式將這個新的數值返回了出來,并且執行dom的更新,我們可以在下面的實現一個useState看得更清楚。

  • 實現一個useState

    1. 首先通過上面的原理的解析,setN改變的并非n,而是另一個變量,所以我們創建這個變量_state,并創建myUseState函數:

      image.png

    2. 之后myUseState接收一個初始值,并返回一個數組,注意這個數組的第一項返回的并不是接收到的初始值而是第一步_state,而第二項則是改變_state的函數:

      image.png

      另外需要注意的是,在第一次執行myUseState的時候需要將初始值賦值給_state,而第二次執行的時候則是將之前的_state賦值回_state:
      image.png

    3. 接著useState在更新state的時候會重新渲染Dom,所以我們在setState函數中執行重新渲染的步驟(這里為了方便簡化了更新步驟):

      image.png

    4. 這時候我們自己的useState就實現完成分了,用來測試一下:

      image.png

      結果可見是成功的:
      image.png

      但是此時我們的myUseState存在一個嚴重的bug,如果一個組件內存在多個state,而_state卻只有一個,就會導致多個state都共用了一個狀態,比如下面的組件:
      image.png

      image.png

  • 修復myUseState的bug

    1. 針對上面所說的bug,在組件擁有多個state的情況下,useState的執行存在由上到下的順序,那么我們就可以將_state改造為一個數組,用于存儲多個state,另外還需要新建一個變量index用于表明state在_state中的順序:

      image.png

    2. 之后在myUseState中要做的第一步就是將接收到的state放入到_state中(注意這里和之前一樣,第一次執行放入的是初始state,從第二次開始變成_state中對應的state):

      image.png

    3. 第三步我們在myUseState中創建一個能夠修改_state中對應數據的函數setState并將其返回出來:

      image.png

    4. 之后需要考慮,setState執行的時候會重新渲染組件,所以在這一步中需要重置index:

      image.png

    5. 另外,為了保證每個_state中的state的順序是一致的,所以在myUseState中將state放入到_state之后,將index + 1,這樣我們就修復了之前多個state沖突的問題了:

      image.png

    6. 測試結果和代碼總覽:


      image.png
import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const _state = []
let index = 0

const myUseState = initialValue => {
    const currentIndex = index
    _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex]
    index = index + 1

    const setState = newValue => {
        _state[currentIndex] = newValue
        index = 0
        ReactDOM.render(<App />, document.getElementById('app'))
    }
    return [_state[currentIndex], setState]
}

const App = () => {
    const [n, setN] = myUseState(0)
    const [m, setM] = myUseState(0)

    const clickN = () => {
        setN(n + 1)
    }

    const clickM = () => {
        setM(m + 1)
    }

    return (
        <div>
            <div>{n}</div>
            <button onClick={clickN}>+</button>
            <div>{m}</div>
            <button onClick={clickM}>+</button>
        </div>
    )
}

ReactDOM.render(<App />, document.getElementById('app'))
  • useState的一些其他知識
    1. useState不能在條件語句后使用的原因: 原因根據上面自己實現的myUseState中就能看出來了,如果放在條件語句后使用,那么就有可能打破_state存放state的順序導致state錯亂。
    2. 看下面代碼,雖然用了兩次setN,但實際上點擊按鈕+后實際上數字只會加一:
      image.png

      image.png

      我們可以將clickN中的代碼改成如下,使其變成每次都能+2:
      image.png

      image.png

useEffect和useLayoutEffect的使用及其異同

  • useEffect
    useEffect接收兩個參數,第一個參數是函數,用于當組件內的state產生變化之后執行,而第二個參數(非必傳)是一個數組,接收依賴的state,比如下面的例子,當n變化的時候將會打印出n的數值:

    image.png

    另外useEffetc接收的函數參數可以返回一個函數,這個函數將在該組件注銷時執行,類似于class組件的componentWillUnmount
    例如下面的組件,在組件掛載后會設置一個定時器,每一秒鐘打印一個1出來,當該組件被注銷后,這個定時也會被注銷:
    image.png

    另外還需要注意,usweEffect接收的函數是在組件渲染完畢之后才執行的。

  • useLayoutEffect
    useLayoutEffect用的非常少,這是一個有點像vue的v-cloak的功能,比如下面的代碼,當組件掛載之后,把div里面的文字從value: 0改成value: 1000:

    image.png

    我們看到的效果確實也是這樣的:
    image.png

    但是當你刷新多幾次的時候,仔細觀察就會發現,每次加載進來頁面都會看到value: 0閃爍一下然后變成value: 1000,這是因為useEffect接收的函數是在組件被渲染之后才會執行的。
    這時候要解決這個問題,就需要將useEffect改成useLayoutEffect了,就不會存在這個閃爍的問題,而是直接顯示value: 1000
    image.png

    原因在于,useLayoutEffect接收到的函數參數在組件渲染之前就會被執行,也就是說useEffectuseLayoutEffect功能其實是類似的,但是執行的時機不同,我們可以從下面的執行順序看出來:
    image.png

    打印的順序確實是1 2 3 4:
    image.png

  • 注意:
    雖然說useLayoutEffect能夠在useEffect之前就執行,但是在不改變網頁Dom文字樣式的情況下,還是推薦使用useEffect的,在需要改變網頁Dom文字樣式的情況下再使用useLayoutEffect

useReducer以及useContext

  • useReducer
    useReducer的使用和redux的使用有些類似,useReducer接收兩個參數,第一個是reducer(和redux中的一模一樣),第二個參數是初始state,之后他會返回一個數組,數組第一項是state,第二項是改變state的函數dispatch,比如下面的例子:

    image.png

    測試結果:
    image.png

  • useContext
    useContext需要和createContext結合起來使用,實際上他們所要解決的問題和redux、mobx是類似的,都是夸組件間的數據傳遞,比如下面的例子,存在App組件,一個父親組件,一個兒子組件,我們就通過創建一個Context,并用這個Context將App組件包裹起來,將App組件內的state傳入到Context,使得父親組件和兒子組件都能夠通過useContext拿到App組件的state:

    image.png

  • useReducer和useContext結合搭建狀態管理系統
    使用useContext可以在任意被對應Context包裹的組件中拿到傳入的數據,將其和useReducer結合起來,
    就可以創建一個組件的狀態管理系統,如何搭建可以參考我的這篇文章從零搭建項目(5) --- 前端: 搭建路由和狀態管理

React.memo、useMemo和useCallback

這三個Api通常都在優化組件的時候使用,并且他們使用的都是記憶化函數的原理,關于記憶化函數可以參考我之前寫的這篇文章: 再談js中的函數

  • React.memo
    memo的功能其實之前class組件的pureComponent差不多,但是這個memo是用在函數式組件上的。
    首先我們來看下面的例子,Child組件引用了App組件的狀態m,狀態n和Child組件并無關系:

    image.png

    但實際上我點擊按鈕并執行setN的時候,Child組件也被更新了:
    image.png

    原因是Child組件被App組件所包裹,而執行setN的時候,App組件被重新渲染了,那么在其之中的Child組件自然也就被重新渲染了。
    所以這時候我們就需要用到memo來優化一下,使得我在執行setN的時候,Child組件不會跟著一起被渲染。
    memo的使用也非常簡單,直接用它包裹需要被優化的組件即可,在本例中就是Child組件,所以代碼可以修改為如下:
    image.png

    這時候我們執行setN的時候就不會使得Child組件跟著重新渲染了,只有執行setM的時候Child才會重新渲染:
    image.png

  • useCallback
    在上面使用memo的例子中,存在一個問題,當Child接收的props中存在函數的時候,之前使用memo做的優化就無效了,比如下面的代碼:

    image.png

    結果:
    image.png

    原因和之前一樣,由于App組件的重新渲染,所以const test = () => {}這段代碼也被重新執行了,而test是一個函數,函數是引用類型,所以傳入到Child中的test也和之前的test函數不一樣,導致Child組件重新渲染。
    這時候我們就可以使用useCallback對其進行優化了。
    useCallback接收兩個參數,首參是一個函數,在本例子中就是test函數,第二個參數是一個數組,這個數組接收的是改變這個函數引用的依賴,比如下面例子,m的值被改變的時候,test函數的引用才會被改變,Child組件才會被重新渲染:
    image.png

    優化結果:
    image.png

  • useMemo類似于vue中computed的功能,他接收兩個參數,第一個參數是一個函數并通過計算得出一個state,第二個參數是計算這個state所需要的依賴,比如下面的例子,Child組件接收多一個props: num,這個num是n與m相加得出的:
    image.png

    結果:
    image.png

useRef和forwardRef

  • useRef
    useRef接收一個參數作為初始值,返回一個可變的 ref 對象,這個 ref 對象含有.current屬性,該屬性可以在整個組件色生命周期內不變。
    通常useRef被用作獲取某個Dom,比如下面的例子:

    image.png

    image.png

  • forwardRef
    forwardRef這個函數用的場景相對較少,它主要用于在父組件獲取子組件的Dom作為自己的ref的時候使用,比如下面的例子:

    image.png

    但實際上這樣做是有問題的,會報錯,并且buttonRef也沒有獲取到:
    image.png

    這時候我們就可以使用forwardRef對Button組件進行包裹,forwardRef會為Button組件注入一個新的參數ref:
    image.png

    這時候父組件就可以獲取得到這個子組件的Dom了:
    image.png

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容