hooks的故事(2):閉包陷阱


hooks 的故事(1):閉包陷阱

經典的場景:

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


不管你如何setCount,輸出的count始終是1!

經典的閉包場景

for ( var i=0; i<5; i++ ) {
    setTimeout(()=>{
        console.log(i)
    }, 0)
}//5,5,5,5,5

// 正確的版本

for ( var i=0; i<5; i++ ) {
   (function(i){
         setTimeout(()=>{
            console.log(i)
        }, 0)
   })(i)
}// 0,1,2,3,4

這是一道經典的js題,輸出是5個5,而非 0,1,2,3,4

原因是因為settimeout被放入任務隊列,拿出執行時取到的i就是5

function App(){
    const [count, setCount] = useState(1);
    useEffect(()=>{
        setInterval(()=>{
            console.log(count)
        }, 1000)
    },
       [])
}
graph LR a[初次渲染]-->b[執行App]-->c[usestate設置count為初始1]-->d[useeffect 設置定時器每隔一秒打印count]
graph LR a[state改變]-->b[試圖更新]-->c[按照fiber鏈表執行hooks]-->d[useEffect deps 不變]

deps不變,就不會重新執行。而一個匿名函數引用了一開始的count(count==1).

這也就是典型的閉包陷阱。變量因為在匿名回調函數引用,形成了一個閉包一直被保存

解決辦法

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

根據我們上面的推測,最直觀的解決就是把狀態直接放到deps數組。

回到我們關於閉包的討論。閉包本質上就是執行現場的保存。所以需要保持函數執行的正確性。關鍵在回調函數執行的時機

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>
  )
}

比如上面這個例子,我們並沒有在useMemo的deps中寫入num1,但執行之后你會發現點擊按鈕之后兩個量都會變!因為回調函數在正確的時機被rerun

為什么useRef每次都可以拿到新鮮的值

一句話,useRef返回的是同一個對象,指向同一片內存

    /* 將這些相關的變量寫在函數外 以模擬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>
  )
}

Object.assign 返回的就是傳入的第一個對象。總兒言之,就是在設置的時候返回了同一個對象。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM