本文不會過多講解基礎知識,更多說的是在使用useRef如何能擺脫 這個 “閉包陷阱” ?
react hooks 的“閉包陷阱” 基本每個開發員都有遇見,這是很令人抓狂的。
(以下react示范demo,均為react 16.8.3 版本)
列一個具體的場景:
function App(){ const [count, setCount] = useState(1); useEffect(()=>{ setInterval(()=>{ console.log(count) }, 1000) }, []) }
其實結果都回事1。不相信就可以看。咋樣是不是很懵。我們先淺談一些hooks其他的特性。
1、一個熟悉的閉包場景
首先從一個各位jser都很熟悉的場景入手。
for ( var i=0; i<5; i++ ) { setTimeout(()=>{ console.log(i) }, 0) }
這樣打印的都是5的原因了。現在就直接貼出使用閉包打印 0...4的代碼:
for ( var i=0; i<5; i++ ) { (function(i){ setTimeout(()=>{ console.log(i) }, 0) })(i) }
這個原理其實就是使用閉包,定時器的回調函數去引用立即執行函數里定義的變量,形成閉包保存了立即執行函數執行時 i 的值,異步定時器的回調函數才如我們想要的打印了順序的值。
其實,useEffect的哪個場景的原因,跟這個,簡直是一樣的,useEffect閉包陷阱場景的出現,是 react 組件更新流程以及useEffect的實現的自然而然結果。
2 淺談hooks原理,理解useEffect 的 “閉包陷阱” 出現原因
這里有很多內容,不細說,大家可以自己深入了解。
首先,可能都聽過react的 Fiber 架構,其實一個 Fiber節點就對應的是一個組件。對于classComponent而言,有state是一件很正常的事情,Fiber對象上有一個memoizedState用于存放組件的state。
ok,現在看 hooks 所針對的FunctionComponnet。 無論開發者怎么折騰,一個對象都只能有一個state屬性或者memoizedState屬性,可是,誰知道可愛的開發者們會在FunctionComponent里寫上多少個useState,useEffect等等 ? 所以,react用了鏈表這種數據結構來存儲FunctionComponent里面的 hooks。比如:
function App(){ const [count, setCount] = useState(1) const [name, setName] = useState('chechengyi') useEffect(()=>{ }, []) const text = useMemo(()=>{ return 'ffffd' }, []) }
在組件第一次渲染的時候,為每個hooks都創建了一個對象
type Hook = { memoizedState: any, baseState: any, baseUpdate: Update<any, any> | null, queue: UpdateQueue<any, any> | null, next: Hook | null, };
最終形成了一個鏈表。
這個對象的memoizedState屬性就是用來存儲組件上一次更新后的state,next毫無疑問是指向下一個hook對象。在組件更新的過程中,hooks函數執行的順序是不變的,就可以根據這個鏈表拿到當前hooks對應的Hook對象,函數式組件就是這樣擁有了state的能力。這里就不展開細說。
其實這刻意解釋就是為什么不能將hooks寫到if else語句中了把?因為這樣可能會導致順序錯亂,導致當前hooks拿到的不是自己對應的Hook對象。
useEffect接收了兩個參數,一個回調函數和一個數組。數組里面就是useEffect的依賴,當為 [] 的時候,回調函數只會在組件第一次渲染的時候執行一次。如果有依賴其他項,react 會判斷其依賴是否改變,如果改變了就會執行回調函數。說回最初的場景:
function App(){ const [count, setCount] = useState(1); useEffect(()=>{ setInterval(()=>{ console.log(count) }, 1000) }, []) function click(){ setCount(2) } }
現在構建,組件第一次渲染執行App(),執行useState設置了初始狀態為1,所以此時的count為1。然后執行了useEffect,回調函數執行,設置了一個定時器每隔 1s 打印一次count。
當click函數被觸發了,調用setCount(2)肯定會觸發react的更新,更新到當前組件的時候也是執行App(),之前說的鏈表已經形成了哈,此時useState將Hook對象 上保存的狀態置為2, 那么此時count也為2了。然后在執行useEffect由于依賴數組是一個空的數組,所以此時回調并不會被執行。
但又有一個新問題。這次更新的過程中根本就沒有涉及到這個定時器,這個定時器還在堅持的,默默的,每隔1s打印一次count。 注意這里打印的count,是組件第一次渲染的時候App()時的count,count的值為1,因為在定時器的回調函數里面被引用了,形成了閉包一直被保存。
2 難道真的要在依賴數組里寫上的值,才能拿到新鮮的值?
仿佛都習慣性都去認為,只有在依賴數組里寫上我們所需要的值,才能在更新的過程中拿到最新鮮的值。那么看一下這個場景:
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,它的依賴數組里面只有num2,沒有num1,卻同時使用了這兩個state。當點擊button 的時候,num1和num2的值都改變了。那么,只寫明了依賴num2的 text 中能否拿到 num1 最新鮮的值呢?
如果你裝了react的 eslint 插件,這里也許會提示你錯誤,因為在text中你使用了 num1 卻沒有在依賴數組中添加它。 但是執行這段代碼會發現,是可以正常拿到num1最新鮮的值的。
其實這要回過頭看看之前說的第一點“閉包陷阱”問題,也就不難理解。
現在再重復一遍,這個依賴數組存在的意義,是react為了判定,在本次更新中,是否需要執行其中的回調函數,這里依賴了的num2,而num2改變了。回調函數自然會執行, 這時形成的閉包引用的就是最新的num1和num2,所以,自然能夠拿到新鮮的值。問題的關鍵,在于回調函數執行的時機,閉包就像是一個照相機,把回調函數執行的那個時機的那些值保存了下來。之前說的定時器的回調函數我想就像是一個從1000年前穿越到現代的人,雖然來到了現代,但是身上的血液、頭發都是1000年前的。
3 為什么使用useRef能夠每次拿到新鮮的值?
看看下面代碼:
var A = {name: 'chechengyi'} var B = A B.name = 'baobao' console.log(A.name) // baobao
對,這就是這個場景成立的最根本原因。
也就是說,在組件每一次渲染的過程中。 比如ref = useRef()所返回的都是同一個對象,每次組件更新所生成的ref指向的都是同一片內存空間, 那么當然能夠每次都拿到最新鮮的值了。其實與犬夜叉中一口古井連接了現代世界與500年前的戰國時代,這個同一個對象也將這些個被保存于不同閉包時機的變量了聯系了起來。
使用一個例子或許好理解一點:
/* 將這些相關的變量寫在函數外 以模擬react hooks對應的對象 */ let isC = false let isInit = true; // 模擬組件第一次加載 let ref = { current: null } function useEffect(cb){ // 這里用來模擬 useEffect 依賴為 [] 的時候只執行一次。 if (isC) return isC = true cb() } function useRef(value){ // 組件是第一次加載的話設置值 否則直接返回對象 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) }) } // 連續執行兩次 第一次組件加載 第二次組件更新 App() App()
所以,提出一個合理的設想。只要我們能保證每次組件更新的時候,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> ) }
簡單說下這段代碼,在執行setObj的時候,傳入的是一個函數。這種用法就不用我多說了把?然后Object.assign返回的就是傳入的第一個對象。總兒言之,就是在設置的時候返回了同一個對象。
執行這段代碼發現,確實點擊button后,定時器打印的值也變成了:
{ name: 'baobao', age: 24 }
4 完畢
其實“閉包陷阱” 淺談 react hooks 更是一種總結,掌握知識、項目經驗都是缺一不可的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/128260.html
摘要:理解的函數基礎要搞好深入淺出原型使用原型模型,雖然這經常被當作缺點提及,但是只要善于運用,其實基于原型的繼承模型比傳統的類繼承還要強大。中文指南基本操作指南二繼續熟悉的幾對方法,包括,,。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。 怎樣使用 this 因為本人屬于偽前端,因此文中只看懂了 8 成左右,希望能夠給大家帶來幫助....(據說是阿里的前端妹子寫的) this 的值到底...
摘要:歡迎來我的個人站點性能優化其他優化瀏覽器關鍵渲染路徑開啟性能優化之旅高性能滾動及頁面渲染優化理論寫法對壓縮率的影響唯快不破應用的個優化步驟進階鵝廠大神用直出實現網頁瞬開緩存網頁性能管理詳解寫給后端程序員的緩存原理介紹年底補課緩存機制優化動 歡迎來我的個人站點 性能優化 其他 優化瀏覽器關鍵渲染路徑 - 開啟性能優化之旅 高性能滾動 scroll 及頁面渲染優化 理論 | HTML寫法...
閱讀 547·2023-03-27 18:33
閱讀 732·2023-03-26 17:27
閱讀 630·2023-03-26 17:14
閱讀 591·2023-03-17 21:13
閱讀 521·2023-03-17 08:28
閱讀 1801·2023-02-27 22:32
閱讀 1292·2023-02-27 22:27
閱讀 2178·2023-01-20 08:28