前言
自react16.8發(fā)布了正式版hook用法以來,我們公司組件的寫法逐漸由class向函數(shù)式組件+hook的方向轉(zhuǎn)移,雖然用了這么久的hook,但是用得多的基本就useState、useEffect和useMemo,其他的官方hook因?yàn)槭褂脠鼍安幻鲗?dǎo)致基本沒用過,所以這兩天特地去了解了一下其他hook的使用場景以及useState的原理,然后用這篇文章記錄一下。
useState的使用及其原理
在hook版本出來之前,react函數(shù)組件無法擁有自身內(nèi)部的狀態(tài),而useState賦予了函數(shù)組件擁有內(nèi)部狀態(tài)的能力,并且它的使用非常簡單。
-
useState用法
useState是一個(gè)函數(shù),它接收一個(gè)初始值,并返回一個(gè)數(shù)組,該數(shù)組的第一位是一個(gè)state,第二位則是改變這個(gè)的state的函數(shù),比如下面這個(gè)計(jì)數(shù)器的例子,當(dāng)我點(diǎn)擊按鈕+的時(shí)候,數(shù)字就+1:
image.png
上例中的n就是這個(gè)函數(shù)組件的內(nèi)部狀態(tài)了。 -
useState原理
在上面的例子中,我們每次點(diǎn)擊按鈕+的時(shí)候,數(shù)字都會(huì)增加1,也就是說App這個(gè)函數(shù)會(huì)被重新執(zhí)行一次:
image.png
image.png
既然App會(huì)被重新執(zhí)行,那么useState(0)也會(huì)被重新執(zhí)行一次,但是為什么n的值不會(huì)被重置為0呢?
原因是,第二次useState執(zhí)行后返回的n并非之前的n,setN改變的并不是之前返回出來的那個(gè)n,setN改變的數(shù)值存儲(chǔ)于其它地方而非n,之后useState通過閉包的形式將這個(gè)新的數(shù)值返回了出來,并且執(zhí)行dom的更新,我們可以在下面的實(shí)現(xiàn)一個(gè)useState看得更清楚。 -
實(shí)現(xiàn)一個(gè)useState
-
首先通過上面的原理的解析,
setN改變的并非n,而是另一個(gè)變量,所以我們創(chuàng)建這個(gè)變量_state,并創(chuàng)建myUseState函數(shù):
image.png -
之后
myUseState接收一個(gè)初始值,并返回一個(gè)數(shù)組,注意這個(gè)數(shù)組的第一項(xiàng)返回的并不是接收到的初始值而是第一步_state,而第二項(xiàng)則是改變_state的函數(shù):
image.png
另外需要注意的是,在第一次執(zhí)行myUseState的時(shí)候需要將初始值賦值給_state,而第二次執(zhí)行的時(shí)候則是將之前的_state賦值回_state:
image.png -
接著
useState在更新state的時(shí)候會(huì)重新渲染Dom,所以我們在setState函數(shù)中執(zhí)行重新渲染的步驟(這里為了方便簡化了更新步驟):
image.png -
這時(shí)候我們自己的
useState就實(shí)現(xiàn)完成分了,用來測試一下:
image.png
結(jié)果可見是成功的:
image.png
但是此時(shí)我們的myUseState存在一個(gè)嚴(yán)重的bug,如果一個(gè)組件內(nèi)存在多個(gè)state,而_state卻只有一個(gè),就會(huì)導(dǎo)致多個(gè)state都共用了一個(gè)狀態(tài),比如下面的組件:
image.png
image.png
-
-
修復(fù)myUseState的bug
-
針對(duì)上面所說的bug,在組件擁有多個(gè)state的情況下,
useState的執(zhí)行存在由上到下的順序,那么我們就可以將_state改造為一個(gè)數(shù)組,用于存儲(chǔ)多個(gè)state,另外還需要新建一個(gè)變量index用于表明state在_state中的順序:
image.png -
之后在
myUseState中要做的第一步就是將接收到的state放入到_state中(注意這里和之前一樣,第一次執(zhí)行放入的是初始state,從第二次開始變成_state中對(duì)應(yīng)的state):
image.png -
第三步我們在
myUseState中創(chuàng)建一個(gè)能夠修改_state中對(duì)應(yīng)數(shù)據(jù)的函數(shù)setState并將其返回出來:
image.png -
之后需要考慮,
setState執(zhí)行的時(shí)候會(huì)重新渲染組件,所以在這一步中需要重置index:
image.png -
另外,為了保證每個(gè)
_state中的state的順序是一致的,所以在myUseState中將state放入到_state之后,將index + 1,這樣我們就修復(fù)了之前多個(gè)state沖突的問題了:
image.png -
測試結(jié)果和代碼總覽:
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的一些其他知識(shí)
-
useState不能在條件語句后使用的原因: 原因根據(jù)上面自己實(shí)現(xiàn)的myUseState中就能看出來了,如果放在條件語句后使用,那么就有可能打破_state存放state的順序?qū)е聅tate錯(cuò)亂。 - 看下面代碼,雖然用了兩次
setN,但實(shí)際上點(diǎn)擊按鈕+后實(shí)際上數(shù)字只會(huì)加一:
image.png
image.png
我們可以將clickN中的代碼改成如下,使其變成每次都能+2:
image.png
image.png
-
useEffect和useLayoutEffect的使用及其異同
-
useEffect
useEffect接收兩個(gè)參數(shù),第一個(gè)參數(shù)是函數(shù),用于當(dāng)組件內(nèi)的state產(chǎn)生變化之后執(zhí)行,而第二個(gè)參數(shù)(非必傳)是一個(gè)數(shù)組,接收依賴的state,比如下面的例子,當(dāng)n變化的時(shí)候?qū)?huì)打印出n的數(shù)值:
image.png
另外useEffetc接收的函數(shù)參數(shù)可以返回一個(gè)函數(shù),這個(gè)函數(shù)將在該組件注銷時(shí)執(zhí)行,類似于class組件的componentWillUnmount。
例如下面的組件,在組件掛載后會(huì)設(shè)置一個(gè)定時(shí)器,每一秒鐘打印一個(gè)1出來,當(dāng)該組件被注銷后,這個(gè)定時(shí)也會(huì)被注銷:
image.png
另外還需要注意,usweEffect接收的函數(shù)是在組件渲染完畢之后才執(zhí)行的。 -
useLayoutEffect
useLayoutEffect用的非常少,這是一個(gè)有點(diǎn)像vue的v-cloak的功能,比如下面的代碼,當(dāng)組件掛載之后,把div里面的文字從value: 0改成value: 1000:
image.png
我們看到的效果確實(shí)也是這樣的:
image.png
但是當(dāng)你刷新多幾次的時(shí)候,仔細(xì)觀察就會(huì)發(fā)現(xiàn),每次加載進(jìn)來頁面都會(huì)看到value: 0閃爍一下然后變成value: 1000,這是因?yàn)?code>useEffect接收的函數(shù)是在組件被渲染之后才會(huì)執(zhí)行的。
這時(shí)候要解決這個(gè)問題,就需要將useEffect改成useLayoutEffect了,就不會(huì)存在這個(gè)閃爍的問題,而是直接顯示value: 1000:
image.png
原因在于,useLayoutEffect接收到的函數(shù)參數(shù)在組件渲染之前就會(huì)被執(zhí)行,也就是說useEffect和useLayoutEffect功能其實(shí)是類似的,但是執(zhí)行的時(shí)機(jī)不同,我們可以從下面的執(zhí)行順序看出來:
image.png
打印的順序確實(shí)是1 2 3 4:
image.png 注意:
雖然說useLayoutEffect能夠在useEffect之前就執(zhí)行,但是在不改變網(wǎng)頁Dom文字樣式的情況下,還是推薦使用useEffect的,在需要改變網(wǎng)頁Dom文字樣式的情況下再使用useLayoutEffect
useReducer以及useContext
-
useReducer
useReducer的使用和redux的使用有些類似,useReducer接收兩個(gè)參數(shù),第一個(gè)是reducer(和redux中的一模一樣),第二個(gè)參數(shù)是初始state,之后他會(huì)返回一個(gè)數(shù)組,數(shù)組第一項(xiàng)是state,第二項(xiàng)是改變state的函數(shù)dispatch,比如下面的例子:
image.png
測試結(jié)果:
image.png -
useContext
useContext需要和createContext結(jié)合起來使用,實(shí)際上他們所要解決的問題和redux、mobx是類似的,都是夸組件間的數(shù)據(jù)傳遞,比如下面的例子,存在App組件,一個(gè)父親組件,一個(gè)兒子組件,我們就通過創(chuàng)建一個(gè)Context,并用這個(gè)Context將App組件包裹起來,將App組件內(nèi)的state傳入到Context,使得父親組件和兒子組件都能夠通過useContext拿到App組件的state:
image.png useReducer和useContext結(jié)合搭建狀態(tài)管理系統(tǒng)
使用useContext可以在任意被對(duì)應(yīng)Context包裹的組件中拿到傳入的數(shù)據(jù),將其和useReducer結(jié)合起來,
就可以創(chuàng)建一個(gè)組件的狀態(tài)管理系統(tǒng),如何搭建可以參考我的這篇文章從零搭建項(xiàng)目(5) --- 前端: 搭建路由和狀態(tài)管理
React.memo、useMemo和useCallback
這三個(gè)Api通常都在優(yōu)化組件的時(shí)候使用,并且他們使用的都是記憶化函數(shù)的原理,關(guān)于記憶化函數(shù)可以參考我之前寫的這篇文章: 再談js中的函數(shù)
-
React.memo
memo的功能其實(shí)之前class組件的pureComponent差不多,但是這個(gè)memo是用在函數(shù)式組件上的。
首先我們來看下面的例子,Child組件引用了App組件的狀態(tài)m,狀態(tài)n和Child組件并無關(guān)系:
image.png
但實(shí)際上我點(diǎn)擊按鈕并執(zhí)行setN的時(shí)候,Child組件也被更新了:
image.png
原因是Child組件被App組件所包裹,而執(zhí)行setN的時(shí)候,App組件被重新渲染了,那么在其之中的Child組件自然也就被重新渲染了。
所以這時(shí)候我們就需要用到memo來優(yōu)化一下,使得我在執(zhí)行setN的時(shí)候,Child組件不會(huì)跟著一起被渲染。
memo的使用也非常簡單,直接用它包裹需要被優(yōu)化的組件即可,在本例中就是Child組件,所以代碼可以修改為如下:
image.png
這時(shí)候我們執(zhí)行setN的時(shí)候就不會(huì)使得Child組件跟著重新渲染了,只有執(zhí)行setM的時(shí)候Child才會(huì)重新渲染:
image.png -
useCallback
在上面使用memo的例子中,存在一個(gè)問題,當(dāng)Child接收的props中存在函數(shù)的時(shí)候,之前使用memo做的優(yōu)化就無效了,比如下面的代碼:
image.png
結(jié)果:
image.png
原因和之前一樣,由于App組件的重新渲染,所以const test = () => {}這段代碼也被重新執(zhí)行了,而test是一個(gè)函數(shù),函數(shù)是引用類型,所以傳入到Child中的test也和之前的test函數(shù)不一樣,導(dǎo)致Child組件重新渲染。
這時(shí)候我們就可以使用useCallback對(duì)其進(jìn)行優(yōu)化了。
useCallback接收兩個(gè)參數(shù),首參是一個(gè)函數(shù),在本例子中就是test函數(shù),第二個(gè)參數(shù)是一個(gè)數(shù)組,這個(gè)數(shù)組接收的是改變這個(gè)函數(shù)引用的依賴,比如下面例子,m的值被改變的時(shí)候,test函數(shù)的引用才會(huì)被改變,Child組件才會(huì)被重新渲染:
image.png
優(yōu)化結(jié)果:
image.png
- useMemo類似于vue中computed的功能,他接收兩個(gè)參數(shù),第一個(gè)參數(shù)是一個(gè)函數(shù)并通過計(jì)算得出一個(gè)state,第二個(gè)參數(shù)是計(jì)算這個(gè)state所需要的依賴,比如下面的例子,Child組件接收多一個(gè)props:
num,這個(gè)num是n與m相加得出的:
image.png
結(jié)果:
image.png
useRef和forwardRef
-
useRef
useRef接收一個(gè)參數(shù)作為初始值,返回一個(gè)可變的 ref 對(duì)象,這個(gè) ref 對(duì)象含有.current屬性,該屬性可以在整個(gè)組件色生命周期內(nèi)不變。
通常useRef被用作獲取某個(gè)Dom,比如下面的例子:
image.png
image.png -
forwardRef
forwardRef這個(gè)函數(shù)用的場景相對(duì)較少,它主要用于在父組件獲取子組件的Dom作為自己的ref的時(shí)候使用,比如下面的例子:
image.png
但實(shí)際上這樣做是有問題的,會(huì)報(bào)錯(cuò),并且buttonRef也沒有獲取到:
image.png
這時(shí)候我們就可以使用forwardRef對(duì)Button組件進(jìn)行包裹,forwardRef會(huì)為Button組件注入一個(gè)新的參數(shù)ref:
image.png
這時(shí)候父組件就可以獲取得到這個(gè)子組件的Dom了:
image.png














































