前言
自react16.8發布了正式版hook用法以來,我們公司組件的寫法逐漸由class向函數式組件+hook的方向轉移,雖然用了這么久的hook,但是用得多的基本就useState
、useEffect
和useMemo
,其他的官方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
-
首先通過上面的原理的解析,
setN
改變的并非n,而是另一個變量,所以我們創建這個變量_state
,并創建myUseState
函數:
image.png -
之后
myUseState
接收一個初始值,并返回一個數組,注意這個數組的第一項返回的并不是接收到的初始值而是第一步_state
,而第二項則是改變_state
的函數:
image.png
另外需要注意的是,在第一次執行myUseState
的時候需要將初始值賦值給_state
,而第二次執行的時候則是將之前的_state
賦值回_state
:
image.png -
接著
useState
在更新state的時候會重新渲染Dom,所以我們在setState
函數中執行重新渲染的步驟(這里為了方便簡化了更新步驟):
image.png -
這時候我們自己的
useState
就實現完成分了,用來測試一下:
image.png
結果可見是成功的:
image.png
但是此時我們的myUseState
存在一個嚴重的bug,如果一個組件內存在多個state,而_state
卻只有一個,就會導致多個state都共用了一個狀態,比如下面的組件:
image.png
image.png
-
-
修復myUseState的bug
-
針對上面所說的bug,在組件擁有多個state的情況下,
useState
的執行存在由上到下的順序,那么我們就可以將_state
改造為一個數組,用于存儲多個state,另外還需要新建一個變量index
用于表明state在_state
中的順序:
image.png -
之后在
myUseState
中要做的第一步就是將接收到的state放入到_state
中(注意這里和之前一樣,第一次執行放入的是初始state,從第二次開始變成_state
中對應的state):
image.png -
第三步我們在
myUseState
中創建一個能夠修改_state
中對應數據的函數setState
并將其返回出來:
image.png -
之后需要考慮,
setState
執行的時候會重新渲染組件,所以在這一步中需要重置index
:
image.png -
另外,為了保證每個
_state
中的state
的順序是一致的,所以在myUseState
中將state放入到_state
之后,將index + 1
,這樣我們就修復了之前多個state沖突的問題了:
image.png -
測試結果和代碼總覽:
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的一些其他知識
-
useState
不能在條件語句后使用的原因: 原因根據上面自己實現的myUseState
中就能看出來了,如果放在條件語句后使用,那么就有可能打破_state
存放state
的順序導致state錯亂。 - 看下面代碼,雖然用了兩次
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
接收到的函數參數在組件渲染之前就會被執行,也就是說useEffect
和useLayoutEffect
功能其實是類似的,但是執行的時機不同,我們可以從下面的執行順序看出來:
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