從react hooks「閉包陷阱」切入,淺談react hooks

首先,本文並不會講解 hooks 的基本用法, 本文從 一個hooks中 「奇怪」(其實符合邏輯) 的 「閉包陷阱」 的場景切入,試圖講清楚其背後的因果。同時,在許多 react hooks 奇技淫巧的文章裏,也能看到 useRef 的身影,那麼爲何使用 useRef 又能擺脫 這個 「閉包陷阱」 ? 我想搞清楚這些問題,將能較大的提高對 react hooks 的理解。react

react hooks 一出現便受到了許多開發人員的追捧,或許在使用react hooks 的時候遇到 「閉包陷阱」 是每一個開發人員在開發的時候都遇到過的事情,有的兩眼懵逼、有的則穩如老狗瞬間就定義到了問題出如今何處。面試

(如下react示範demo,均爲react 16.8.3 版本)數組

你必定遭遇過如下這個場景:數據結構

function App(){
    const [count, setCount] = useState(1);
    useEffect(()=>{
        setInterval(()=>{
            console.log(count)
        }, 1000)
    }, [])
}
複製代碼

在這個定時器裏面去打印 count 的值,會發現,無論在這個組件中的其餘地方使用 setCountcount 設置爲任何值,仍是設置多少次,打印的都是1。是否是有一種,儘管歷經千帆,我記得的仍是你當初的模樣的感受? hhh... 接下來,我將盡力的嘗試將我理解的,爲何會發生這麼個狀況說清楚,而且淺談一些hooks其餘的特性。若是有錯誤,但願各位同窗能救救孩子,不要讓我帶着錯誤的認知活下去了。。。閉包

一、一個熟悉的閉包場景

首先從一個各位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 的值,異步定時器的回調函數才如咱們想要的打印了順序的值。ui

其實,useEffect 的哪一個場景的緣由,跟這個,簡直是同樣的,useEffect 閉包陷阱場景的出現,是 react 組件更新流程以及 useEffect 的實現的天然而然結果spa

2 淺談hooks原理,理解useEffect 的 「閉包陷阱」 出現緣由。

其實,很不想在寫這篇文章的過程當中,牽扯到react原理這方面的東西,由於真的是太總體了(其實主要緣由是菜,本身也只是掌握的囫圇吞棗),你要明白這個大概的過程,你得明白支撐起這個大概的一些重要的點。

首先,可能都聽過react的 Fiber 架構,其實一個 Fiber節點就對應的是一個組件。對於 classComponent 而言,有 state 是一件很正常的事情,Fiber對象上有一個 memoizedState 用於存放組件的 state。ok,如今看 hooks 所針對的 FunctionComponnet。 不管開發者怎麼折騰,一個對象都只能有一個 state 屬性或者 memoizedState 屬性,但是,誰知道可愛的開發者們會在 FunctionComponent 裏寫上多少個 useStateuseEffect 等等 ? 因此,react用了鏈表這種數據結構來存儲 FunctionComponent 裏面的 hooks。好比:

function App(){
    const [count, setCount] = useState(1)
    const [name, setName] = useState('chechengyi')
    useEffect(()=>{
        
    }, [])
    const text = useMemo(()=>{
        return 'ddd'
    }, [])
}
複製代碼

在組件第一次渲染的時候,爲每一個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(),以前說的鏈表已經造成了哈,此時 useStateHook 對象 上保存的狀態置爲2, 那麼此時 count 也爲2了。而後在執行 useEffect 因爲依賴數組是一個空的數組,因此此時回調並不會被執行。

ok,此次更新的過程當中根本就沒有涉及到這個定時器,這個定時器還在堅持的,默默的,每隔1s打印一次 count。 注意這裏打印的 count ,是組件第一次渲染的時候 App() 時的 countcount的值爲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可以每次拿到新鮮的值?

大白話說:由於初始化的 useRef 執行以後,返回的都是同一個對象。寫到這裏寶寶又不由回憶起剛學js那會兒,捧着紅寶書啃時候的場景了:

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 全文再此就結束了。 反正寫完了這篇文章,寶寶我對 hooks 的認識是比之前深了。 但願也能對其餘以前跟我有一樣疑惑的人有所幫助。 若是對你有幫助,答應我,請不要吝嗇你的贊好嗎。

相關文章
相關標籤/搜索