日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網(wǎng)站首頁 編程語言 正文

react?hooks閉包陷阱切入淺談_React

作者:Chechengyi ? 更新時間: 2022-09-02 編程語言

引言

首先,本文并不會講解 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),不管在這個組件中的其他地方使用 setCountcount 設置為任何值,還是設置多少次,打印的都是1。是不是有一種,盡管歷經千帆,我記得的還是你當初的模樣的感覺? hhh... 接下來,我將盡力的嘗試將我理解的,為什么會發(fā)生這么個情況說清楚,并且淺談一些hooks其他的特性。如果有錯誤,希望各位同學能救救孩子,不要讓我?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)
}

這個原理其實就是使用閉包,定時器的回調函數(shù)去引用立即執(zhí)行函數(shù)里定義的變量,形成閉包保存了立即執(zhí)行函數(shù)執(zhí)行時 i 的值,異步定時器的回調函數(shù)才如我們想要的打印了順序的值。

其實,useEffect 的哪個場景的原因,跟這個,簡直是一樣的,useEffect 閉包陷阱場景的出現(xiàn),是 react 組件更新流程以及 useEffect 的實現(xiàn)的自然而然結果。

2 淺談hooks原理,理解useEffect 的 “閉包陷阱” 出現(xiàn)原因

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

首先,可能都聽過react的 Fiber 架構,其實一個 Fiber節(jié)點就對應的是一個組件。對于 classComponent 而言,有 state 是一件很正常的事情,F(xiàn)iber對象上有一個 memoizedState 用于存放組件的 state。

ok,現(xiàn)在看 hooks 所針對的 FunctionComponnet。 無論開發(fā)者怎么折騰,一個對象都只能有一個 state 屬性或者 memoizedState 屬性,可是,誰知道可愛的開發(fā)者們會在 FunctionComponent 里寫上多少個 useState,useEffect 等等 ? 所以,react用了鏈表這種數(shù)據(jù)結構來存儲 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ù)這個鏈表拿到當前hooks對應的Hook對象,函數(shù)式組件就是這樣擁有了state的能力。當前,具體的實現(xiàn)肯定比這三言兩語復雜很多。

所以,知道為什么不能將hooks寫到if else語句中了把?因為這樣可能會導致順序錯亂,導致當前hooks拿到的不是自己對應的Hook對象。

useEffect 接收了兩個參數(shù),一個回調函數(shù)和一個數(shù)組。數(shù)組里面就是 useEffect 的依賴,當為 [] 的時候,回調函數(shù)只會在組件第一次渲染的時候執(zhí)行一次。如果有依賴其他項,react 會判斷其依賴是否改變,如果改變了就會執(zhí)行回調函數(shù)。說回最初的場景:

function App(){
    const [count, setCount] = useState(1);
    useEffect(()=&gt;{
        setInterval(()=&gt;{
            console.log(count)
        }, 1000)
    }, [])
    function click(){ setCount(2) }
}

好,開動腦袋開始想象起來,組件第一次渲染執(zhí)行 App(),執(zhí)行 useState 設置了初始狀態(tài)為1,所以此時的 count 為1。然后執(zhí)行了 useEffect,回調函數(shù)執(zhí)行,設置了一個定時器每隔 1s 打印一次 count。

接著想象如果 click 函數(shù)被觸發(fā)了,調用 setCount(2) 肯定會觸發(fā)react的更新,更新到當前組件的時候也是執(zhí)行 App(),之前說的鏈表已經形成了哈,此時 useStateHook 對象 上保存的狀態(tài)置為2, 那么此時 count 也為2了。然后在執(zhí)行 useEffect 由于依賴數(shù)組是一個空的數(shù)組,所以此時回調并不會被執(zhí)行。

ok,這次更新的過程中根本就沒有涉及到這個定時器,這個定時器還在堅持的,默默的,每隔1s打印一次 count。 注意這里打印的 count ,是組件第一次渲染的時候 App() 時的 count, count的值為1,因為在定時器的回調函數(shù)里面被引用了,形成了閉包一直被保存。

2 難道真的要在依賴數(shù)組里寫上的值,才能拿到新鮮的值?

仿佛都習慣性都去認為,只有在依賴數(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。當點擊button 的時候,num1和num2的值都改變了。那么,只寫明了依賴num2的 text 中能否拿到 num1 最新鮮的值呢?

如果你裝了 react 的 eslint 插件,這里也許會提示你錯誤,因為在text中你使用了 num1 卻沒有在依賴數(shù)組中添加它。 但是執(zhí)行這段代碼會發(fā)現(xiàn),是可以正常拿到num1最新鮮的值的。

如果理解了之前第一點說的“閉包陷阱”問題,肯定也能理解這個問題。

為什么呢,再說一遍,這個依賴數(shù)組存在的意義,是react為了判定,在本次更新中,是否需要執(zhí)行其中的回調函數(shù),這里依賴了的num2,而num2改變了。回調函數(shù)自然會執(zhí)行, 這時形成的閉包引用的就是最新的num1和num2,所以,自然能夠拿到新鮮的值。問題的關鍵,在于回調函數(shù)執(zhí)行的時機,閉包就像是一個照相機,把回調函數(shù)執(zhí)行的那個時機的那些值保存了下來。之前說的定時器的回調函數(shù)我想就像是一個從1000年前穿越到現(xiàn)代的人,雖然來到了現(xiàn)代,但是身上的血液、頭發(fā)都是1000年前的。

3 為什么使用useRef能夠每次拿到新鮮的值?

大白話說:因為初始化的 useRef 執(zhí)行之后,返回的都是同一個對象。寫到這里寶寶又不禁回憶起剛學js那會兒,捧著紅寶書啃時候的場景了:

var A = {name: 'chechengyi'}
var B = A
B.name = 'baobao'
console.log(A.name) // baobao

對,這就是這個場景成立的最根本原因。

也就是說,在組件每一次渲染的過程中。 比如 ref = useRef() 所返回的都是同一個對象,每次組件更新所生成的ref指向的都是同一片內存空間, 那么當然能夠每次都拿到最新鮮的值了。犬夜叉看過把?一口古井連接了現(xiàn)代世界與500年前的戰(zhàn)國時代,這個同一個對象也將這些個被保存于不同閉包時機的變量了聯(lián)系了起來。

使用一個例子或許好理解一點:

    /* 將這些相關的變量寫在函數(shù)外 以模擬react hooks對應的對象 */
	let isC = false
	let isInit = true; // 模擬組件第一次加載
	let ref = {
		current: null
	}
	function useEffect(cb){
		// 這里用來模擬 useEffect 依賴為 [] 的時候只執(zhí)行一次。
 		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)
		})
	}
		// 連續(xù)執(zhí)行兩次 第一次組件加載 第二次組件更新
	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>
  )
}

簡單說下這段代碼,在執(zhí)行 setObj 的時候,傳入的是一個函數(shù)。這種用法就不用我多說了把?然后 Object.assign 返回的就是傳入的第一個對象??們貉灾?,就是在設置的時候返回了同一個對象。

執(zhí)行這段代碼發(fā)現(xiàn),確實點擊button后,定時器打印的值也變成了:

{
    name: 'baobao',
    age: 24 
}

4 完畢

原文鏈接:https://juejin.cn/post/6844904193044512782

欄目分類
最近更新