react?hooks閉包陷阱切入淺談
引言
首先,本文并不會講解 hooks 的基本用法, 本文從 一個hooks中 “奇怪”(其實符合邏輯) 的 “閉包陷阱” 的場景切入,試圖講清楚其背后的因果。同時,在許多 react hooks 奇技淫巧的文章里,也能看到 useRef
的身影,那么為什么使用 useRef
又能擺脫 這個 “閉包陷阱” ? 我想搞清楚這些問題,將能較大的提升對 react hooks 的理解。
react hooks 一出現(xiàn)便受到了許多開發(fā)人員的追捧,或許在使用react hooks 的時候遇到 “閉包陷阱” 是每個開發(fā)人員在開發(fā)的時候都遇到過的事情,有的兩眼懵逼、有的則穩(wěn)如老狗瞬間就定義到了問題出現(xiàn)在何處。
(以下react示范demo,均為react 16.8.3 版本)
你一定遭遇過以下這個場景:
function App(){ const [count, setCount] = useState(1); useEffect(()=>{ setInterval(()=>{ console.log(count) }, 1000) }, []) }
在這個定時器里面去打印 count
的值,會發(fā)現(xiàn),不管在這個組件中的其他地方使用 setCount
將 count
設(shè)置為任何值,還是設(shè)置多少次,打印的都是1。是不是有一種,盡管歷經(jīng)千帆,我記得的還是你當(dāng)初的模樣的感覺? hhh... 接下來,我將盡力的嘗試將我理解的,為什么會發(fā)生這么個情況說清楚,并且淺談一些hooks其他的特性。如果有錯誤,希望各位同學(xué)能救救孩子,不要讓我?guī)еe誤的認知活下去了。。。
1、一個熟悉的閉包場景
首先從一個各位jser都很熟悉的場景入手。
for ( var i=0; i<5; i++ ) { setTimeout(()=>{ console.log(i) }, 0) }
想寶寶我剛剛畢業(yè)的那一年,這道題還是一道有些熱門的面試題目。而如今...
我就不說為什么最終,打印的都是5的原因了。直接貼出使用閉包打印 0...4的代碼:
for ( var i=0; i<5; i++ ) { (function(i){ setTimeout(()=>{ console.log(i) }, 0) })(i) }
這個原理其實就是使用閉包,定時器的回調(diào)函數(shù)去引用立即執(zhí)行函數(shù)里定義的變量,形成閉包保存了立即執(zhí)行函數(shù)執(zhí)行時 i 的值,異步定時器的回調(diào)函數(shù)才如我們想要的打印了順序的值。
其實,useEffect
的哪個場景的原因,跟這個,簡直是一樣的,useEffect
閉包陷阱場景的出現(xiàn),是 react 組件更新流程以及 useEffect
的實現(xiàn)的自然而然結(jié)果。
2 淺談hooks原理,理解useEffect 的 “閉包陷阱” 出現(xiàn)原因
其實,很不想在寫這篇文章的過程中,牽扯到react原理這方面的東西,因為真的是太整體了(其實主要原因是菜,自己也只是掌握的囫圇吞棗),你要明白這個大概的過程,你得明白支撐起這個大概的一些重要的點。
首先,可能都聽過react的 Fiber 架構(gòu),其實一個 Fiber節(jié)點就對應(yīng)的是一個組件。對于 classComponent
而言,有 state
是一件很正常的事情,F(xiàn)iber對象上有一個 memoizedState
用于存放組件的 state
。
ok,現(xiàn)在看 hooks 所針對的 FunctionComponnet
。 無論開發(fā)者怎么折騰,一個對象都只能有一個 state
屬性或者 memoizedState
屬性,可是,誰知道可愛的開發(fā)者們會在 FunctionComponent
里寫上多少個 useState
,useEffect
等等 ? 所以,react用了鏈表這種數(shù)據(jù)結(jié)構(gòu)來存儲 FunctionComponent
里面的 hooks。比如:
function App(){ const [count, setCount] = useState(1) const [name, setName] = useState('chechengyi') useEffect(()=>{ }, []) const text = useMemo(()=>{ return 'ddd' }, []) }
在組件第一次渲染的時候,為每個hooks都創(chuàng)建了一個對象
type Hook = { memoizedState: any, baseState: any, baseUpdate: Update<any, any> | null, queue: UpdateQueue<any, any> | null, next: Hook | null, };
最終形成了一個鏈表。
這個對象的memoizedState
屬性就是用來存儲組件上一次更新后的 state
,next
毫無疑問是指向下一個hook對象。在組件更新的過程中,hooks函數(shù)執(zhí)行的順序是不變的,就可以根據(jù)這個鏈表拿到當(dāng)前hooks對應(yīng)的Hook
對象,函數(shù)式組件就是這樣擁有了state的能力。當(dāng)前,具體的實現(xiàn)肯定比這三言兩語復(fù)雜很多。
所以,知道為什么不能將hooks寫到if else語句中了把?因為這樣可能會導(dǎo)致順序錯亂,導(dǎo)致當(dāng)前hooks拿到的不是自己對應(yīng)的Hook對象。
useEffect
接收了兩個參數(shù),一個回調(diào)函數(shù)和一個數(shù)組。數(shù)組里面就是 useEffect
的依賴,當(dāng)為 [] 的時候,回調(diào)函數(shù)只會在組件第一次渲染的時候執(zhí)行一次。如果有依賴其他項,react 會判斷其依賴是否改變,如果改變了就會執(zhí)行回調(diào)函數(shù)。說回最初的場景:
function App(){ const [count, setCount] = useState(1); useEffect(()=>{ setInterval(()=>{ console.log(count) }, 1000) }, []) function click(){ setCount(2) } }
好,開動腦袋開始想象起來,組件第一次渲染執(zhí)行 App()
,執(zhí)行 useState
設(shè)置了初始狀態(tài)為1,所以此時的 count
為1。然后執(zhí)行了 useEffect
,回調(diào)函數(shù)執(zhí)行,設(shè)置了一個定時器每隔 1s 打印一次 count
。
接著想象如果 click
函數(shù)被觸發(fā)了,調(diào)用 setCount(2)
肯定會觸發(fā)react的更新,更新到當(dāng)前組件的時候也是執(zhí)行 App()
,之前說的鏈表已經(jīng)形成了哈,此時 useState
將 Hook
對象 上保存的狀態(tài)置為2, 那么此時 count
也為2了。然后在執(zhí)行 useEffect
由于依賴數(shù)組是一個空的數(shù)組,所以此時回調(diào)并不會被執(zhí)行。
ok,這次更新的過程中根本就沒有涉及到這個定時器,這個定時器還在堅持的,默默的,每隔1s打印一次 count
。 注意這里打印的 count
,是組件第一次渲染的時候 App()
時的 count
, count
的值為1,因為在定時器的回調(diào)函數(shù)里面被引用了,形成了閉包一直被保存。
2 難道真的要在依賴數(shù)組里寫上的值,才能拿到新鮮的值?
仿佛都習(xí)慣性都去認為,只有在依賴數(shù)組里寫上我們所需要的值,才能在更新的過程中拿到最新鮮的值。那么看一下這個場景:
function App() { return <Demo1 /> } function Demo1(){ const [num1, setNum1] = useState(1) const [num2, setNum2] = useState(10) const text = useMemo(()=>{ return `num1: ${num1} | num2:${num2}` }, [num2]) function handClick(){ setNum1(2) setNum2(20) } return ( <div> {text} <div><button onClick={handClick}>click!</button></div> </div> ) }
text
是一個 useMemo
,它的依賴數(shù)組里面只有num2,沒有num1,卻同時使用了這兩個state。當(dāng)點擊button 的時候,num1和num2的值都改變了。那么,只寫明了依賴num2的 text 中能否拿到 num1 最新鮮的值呢?
如果你裝了 react
的 eslint 插件,這里也許會提示你錯誤,因為在text中你使用了 num1 卻沒有在依賴數(shù)組中添加它。 但是執(zhí)行這段代碼會發(fā)現(xiàn),是可以正常拿到num1最新鮮的值的。
如果理解了之前第一點說的“閉包陷阱”問題,肯定也能理解這個問題。
為什么呢,再說一遍,這個依賴數(shù)組存在的意義,是react為了判定,在本次更新中,是否需要執(zhí)行其中的回調(diào)函數(shù),這里依賴了的num2,而num2改變了。回調(diào)函數(shù)自然會執(zhí)行, 這時形成的閉包引用的就是最新的num1和num2,所以,自然能夠拿到新鮮的值。問題的關(guān)鍵,在于回調(diào)函數(shù)執(zhí)行的時機,閉包就像是一個照相機,把回調(diào)函數(shù)執(zhí)行的那個時機的那些值保存了下來。之前說的定時器的回調(diào)函數(shù)我想就像是一個從1000年前穿越到現(xiàn)代的人,雖然來到了現(xiàn)代,但是身上的血液、頭發(fā)都是1000年前的。
3 為什么使用useRef能夠每次拿到新鮮的值?
大白話說:因為初始化的 useRef
執(zhí)行之后,返回的都是同一個對象。寫到這里寶寶又不禁回憶起剛學(xué)js那會兒,捧著紅寶書啃時候的場景了:
var A = {name: 'chechengyi'} var B = A B.name = 'baobao' console.log(A.name) // baobao
對,這就是這個場景成立的最根本原因。
也就是說,在組件每一次渲染的過程中。 比如 ref = useRef()
所返回的都是同一個對象,每次組件更新所生成的ref
指向的都是同一片內(nèi)存空間, 那么當(dāng)然能夠每次都拿到最新鮮的值了。犬夜叉看過把?一口古井連接了現(xiàn)代世界與500年前的戰(zhàn)國時代,這個同一個對象也將這些個被保存于不同閉包時機的變量了聯(lián)系了起來。
使用一個例子或許好理解一點:
/* 將這些相關(guān)的變量寫在函數(shù)外 以模擬react hooks對應(yīng)的對象 */ let isC = false let isInit = true; // 模擬組件第一次加載 let ref = { current: null } function useEffect(cb){ // 這里用來模擬 useEffect 依賴為 [] 的時候只執(zhí)行一次。 if (isC) return isC = true cb() } function useRef(value){ // 組件是第一次加載的話設(shè)置值 否則直接返回對象 if ( isInit ) { ref.current = value isInit = false } return ref } function App(){ let ref_ = useRef(1) ref_.current++ useEffect(()=>{ setInterval(()=>{ console.log(ref.current) // 3 }, 2000) }) } // 連續(xù)執(zhí)行兩次 第一次組件加載 第二次組件更新 App() App()
所以,提出一個合理的設(shè)想。只要我們能保證每次組件更新的時候,useState
返回的是同一個對象的話?我們也能繞開閉包陷阱這個情景嗎? 試一下吧。
function App() { // return <Demo1 /> return <Demo2 /> } function Demo2(){ const [obj, setObj] = useState({name: 'chechengyi'}) useEffect(()=>{ setInterval(()=>{ console.log(obj) }, 2000) }, []) function handClick(){ setObj((prevState)=> { var nowObj = Object.assign(prevState, { name: 'baobao', age: 24 }) console.log(nowObj == prevState) return nowObj }) } return ( <div> <div> <span>name: {obj.name} | age: {obj.age}</span> <div><button onClick={handClick}>click!</button></div> </div> </div> ) }
簡單說下這段代碼,在執(zhí)行 setObj
的時候,傳入的是一個函數(shù)。這種用法就不用我多說了把?然后 Object.assign
返回的就是傳入的第一個對象。總兒言之,就是在設(shè)置的時候返回了同一個對象。
執(zhí)行這段代碼發(fā)現(xiàn),確實點擊button后,定時器打印的值也變成了:
{ name: 'baobao', age: 24 }
4 完畢
通過一次“閉包陷阱” 淺談 react hooks 全文再此就結(jié)束了。 反正寫完了這篇文章,我對 hooks 的認識是比以前深了,更多關(guān)于react hooks閉包的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解react-router 4.0 下服務(wù)器如何配合BrowserRouter
這篇文章主要介紹了詳解react-router 4.0 下服務(wù)器如何配合BrowserRouter,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12React組件中監(jiān)聽函數(shù)獲取不到最新的state問題
這篇文章主要介紹了React組件中監(jiān)聽函數(shù)獲取不到最新的state問題問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01react-native組件中NavigatorIOS和ListView結(jié)合使用的方法
這篇文章主要給大家介紹了關(guān)于react-native組件中NavigatorIOS和ListView結(jié)合使用的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-09-09React使用PropTypes實現(xiàn)類型檢查功能
這篇文章主要介紹了React高級指引中使用PropTypes實現(xiàn)類型檢查功能的方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-02-02