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

學無先后,達者為師

網站首頁 編程語言 正文

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

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

引言

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

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其他的特性。如果有錯誤,希望各位同學能救救孩子,不要讓我帶著錯誤的認知活下去了。。。

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原理這方面的東西,因為真的是太整體了(其實主要原因是菜,自己也只是掌握的囫圇吞棗),你要明白這個大概的過程,你得明白支撐起這個大概的一些重要的點。

首先,可能都聽過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(()=&gt;{
        setInterval(()=&gt;{
            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 完畢

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

欄目分類
最近更新