前言
自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:
上例中的n就是這個(gè)函數(shù)組件的內(nèi)部狀態(tài)了。 -
useState原理
在上面的例子中浑测,我們每次點(diǎn)擊按鈕+的時(shí)候翅阵,數(shù)字都會(huì)增加1,也就是說App這個(gè)函數(shù)會(huì)被重新執(zhí)行一次:
既然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ù):
-
之后
myUseState
接收一個(gè)初始值赋访,并返回一個(gè)數(shù)組,注意這個(gè)數(shù)組的第一項(xiàng)返回的并不是接收到的初始值而是第一步_state
厕氨,而第二項(xiàng)則是改變_state
的函數(shù):
另外需要注意的是进每,在第一次執(zhí)行myUseState
的時(shí)候需要將初始值賦值給_state
,而第二次執(zhí)行的時(shí)候則是將之前的_state
賦值回_state
:
-
接著
useState
在更新state的時(shí)候會(huì)重新渲染Dom命斧,所以我們在setState
函數(shù)中執(zhí)行重新渲染的步驟(這里為了方便簡化了更新步驟):
-
這時(shí)候我們自己的
useState
就實(shí)現(xiàn)完成分了田晚,用來測試一下:
結(jié)果可見是成功的:
但是此時(shí)我們的myUseState
存在一個(gè)嚴(yán)重的bug,如果一個(gè)組件內(nèi)存在多個(gè)state国葬,而_state
卻只有一個(gè)贤徒,就會(huì)導(dǎo)致多個(gè)state都共用了一個(gè)狀態(tài)芹壕,比如下面的組件:
-
-
修復(fù)myUseState的bug
-
針對上面所說的bug,在組件擁有多個(gè)state的情況下接奈,
useState
的執(zhí)行存在由上到下的順序踢涌,那么我們就可以將_state
改造為一個(gè)數(shù)組,用于存儲(chǔ)多個(gè)state序宦,另外還需要新建一個(gè)變量index
用于表明state在_state
中的順序:
-
之后在
myUseState
中要做的第一步就是將接收到的state放入到_state
中(注意這里和之前一樣睁壁,第一次執(zhí)行放入的是初始state,從第二次開始變成_state
中對應(yīng)的state):
-
第三步我們在
myUseState
中創(chuàng)建一個(gè)能夠修改_state
中對應(yīng)數(shù)據(jù)的函數(shù)setState
并將其返回出來:
-
之后需要考慮互捌,
setState
執(zhí)行的時(shí)候會(huì)重新渲染組件潘明,所以在這一步中需要重置index
:
-
另外,為了保證每個(gè)
_state
中的state
的順序是一致的秕噪,所以在myUseState
中將state放入到_state
之后钳降,將index + 1
,這樣我們就修復(fù)了之前多個(gè)state沖突的問題了:
-
測試結(jié)果和代碼總覽:
-
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
不能在條件語句后使用的原因: 原因根據(jù)上面自己實(shí)現(xiàn)的myUseState
中就能看出來了腌巾,如果放在條件語句后使用遂填,那么就有可能打破_state
存放state
的順序?qū)е聅tate錯(cuò)亂。 - 看下面代碼澈蝙,雖然用了兩次
setN
吓坚,但實(shí)際上點(diǎn)擊按鈕+后實(shí)際上數(shù)字只會(huì)加一:
我們可以將clickN
中的代碼改成如下,使其變成每次都能+2:
-
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ù)值:
另外useEffetc
接收的函數(shù)參數(shù)可以返回一個(gè)函數(shù)撕贞,這個(gè)函數(shù)將在該組件注銷時(shí)執(zhí)行更耻,類似于class組件的componentWillUnmount
。
例如下面的組件捏膨,在組件掛載后會(huì)設(shè)置一個(gè)定時(shí)器秧均,每一秒鐘打印一個(gè)1出來,當(dāng)該組件被注銷后号涯,這個(gè)定時(shí)也會(huì)被注銷:
另外還需要注意目胡,usweEffect
接收的函數(shù)是在組件渲染完畢之后才執(zhí)行的。 -
useLayoutEffect
useLayoutEffect
用的非常少链快,這是一個(gè)有點(diǎn)像vue的v-cloak
的功能誉己,比如下面的代碼,當(dāng)組件掛載之后域蜗,把div里面的文字從value: 0
改成value: 1000
:
我們看到的效果確實(shí)也是這樣的:
但是當(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
:
原因在于坪蚁,useLayoutEffect
接收到的函數(shù)參數(shù)在組件渲染之前就會(huì)被執(zhí)行,也就是說useEffect
和useLayoutEffect
功能其實(shí)是類似的婆廊,但是執(zhí)行的時(shí)機(jī)不同迅细,我們可以從下面的執(zhí)行順序看出來:
打印的順序確實(shí)是1 2 3 4
:
注意:
雖然說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帆离,比如下面的例子:
測試結(jié)果:
-
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:
useReducer和useContext結(jié)合搭建狀態(tài)管理系統(tǒng)
使用useContext
可以在任意被對應(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)系:
但實(shí)際上我點(diǎn)擊按鈕并執(zhí)行setN
的時(shí)候泛烙,Child組件也被更新了:
原因是Child組件被App組件所包裹理卑,而執(zhí)行setN
的時(shí)候,App組件被重新渲染了蔽氨,那么在其之中的Child組件自然也就被重新渲染了藐唠。
所以這時(shí)候我們就需要用到memo
來優(yōu)化一下,使得我在執(zhí)行setN
的時(shí)候鹉究,Child組件不會(huì)跟著一起被渲染宇立。
memo
的使用也非常簡單,直接用它包裹需要被優(yōu)化的組件即可自赔,在本例中就是Child組件妈嘹,所以代碼可以修改為如下:
這時(shí)候我們執(zhí)行setN
的時(shí)候就不會(huì)使得Child組件跟著重新渲染了,只有執(zhí)行setM
的時(shí)候Child才會(huì)重新渲染:
-
useCallback
在上面使用memo
的例子中绍妨,存在一個(gè)問題润脸,當(dāng)Child接收的props中存在函數(shù)的時(shí)候,之前使用memo做的優(yōu)化就無效了他去,比如下面的代碼:
結(jié)果:
原因和之前一樣毙驯,由于App組件的重新渲染,所以const test = () => {}
這段代碼也被重新執(zhí)行了灾测,而test是一個(gè)函數(shù)爆价,函數(shù)是引用類型,所以傳入到Child中的test也和之前的test函數(shù)不一樣媳搪,導(dǎo)致Child組件重新渲染铭段。
這時(shí)候我們就可以使用useCallback
對其進(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ì)被重新渲染:
優(yōu)化結(jié)果:
- 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相加得出的:
結(jié)果:
useRef和forwardRef
-
useRef
useRef
接收一個(gè)參數(shù)作為初始值所灸,返回一個(gè)可變的 ref 對象,這個(gè) ref 對象含有.current
屬性炫七,該屬性可以在整個(gè)組件色生命周期內(nèi)不變爬立。
通常useRef
被用作獲取某個(gè)Dom,比如下面的例子:
-
forwardRef
forwardRef
這個(gè)函數(shù)用的場景相對較少万哪,它主要用于在父組件獲取子組件的Dom作為自己的ref的時(shí)候使用侠驯,比如下面的例子:
但實(shí)際上這樣做是有問題的,會(huì)報(bào)錯(cuò)奕巍,并且buttonRef
也沒有獲取到:
這時(shí)候我們就可以使用forwardRef
對Button組件進(jìn)行包裹吟策,forwardRef
會(huì)為Button組件注入一個(gè)新的參數(shù)ref:
這時(shí)候父組件就可以獲取得到這個(gè)子組件的Dom了: