Live2D

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 返回的就是传入的第一个对象。总儿言之,就是在设置的时候返回了同一个对象。

posted @ 2020-09-04 19:50  二胖he鼠标  阅读(1415)  评论(0编辑  收藏  举报