react hooks闭包陷阱切入浅谈

 更新时间:2022年07月11日 14:23:44   作者:Chechengyi  
这篇文章主要介绍了从react hooks闭包陷阱切入浅谈react hooks,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

首先,本文并不会讲解 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 完毕

通过一次“闭包陷阱” 浅谈 react hooks 全文再此就结束了。 反正写完了这篇文章,我对 hooks 的认识是比以前深了,更多关于react hooks闭包的资料请关注脚本之家其它相关文章!

相关文章

  • 详解React中组件之间通信的方式

    详解React中组件之间通信的方式

    这篇文章主要介绍了React中组件之间通信的方式,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-07-07
  • 教你react中如何理解usestate、useEffect副作用、useRef标识和useContext

    教你react中如何理解usestate、useEffect副作用、useRef标识和useContext

    这篇文章主要介绍了react中如何理解usestate、useEffect副作用、useRef标识和useContext,其实与vue中的ref和reactive一样,通过useState获取到的数据可以实现组件视图实时交互,而普通定义的数据仅仅在业务中使用,需要的朋友可以参考下
    2022-11-11
  • react quill中图片上传由默认转成base64改成上传到服务器的方法

    react quill中图片上传由默认转成base64改成上传到服务器的方法

    这篇文章主要介绍了react quill中图片上传由默认转成base64改成上传到服务器的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • VSCode搭建React Native环境

    VSCode搭建React Native环境

    这篇文章主要介绍了VSCode搭建React Native环境,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-05-05
  • React服务端渲染(总结)

    React服务端渲染(总结)

    当我们要求渲染时间尽量快、页面响应速度快时就会用到服务端渲染,本篇文章主要介绍了React服务端渲染,有兴趣的可以了解一下
    2017-07-07
  • 利用React实现一个有点意思的电梯小程序

    利用React实现一个有点意思的电梯小程序

    这篇文章主要为大家详解介绍了如何利用React实现一个有点意思的电梯小程序,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起了解一下
    2022-08-08
  • react实现同页面三级跳转路由布局

    react实现同页面三级跳转路由布局

    这篇文章主要为大家详细介绍了react实现同页面三级跳转路由布局,一个路由小案例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-09-09
  • React.js入门实例教程之创建hello world 的5种方式

    React.js入门实例教程之创建hello world 的5种方式

    React 是近期非常热门的一个前端开发框架。应用非常广泛,接下来通过本文给大家介绍React.js入门实例教程之创建hello world 的5种方式 ,需要的朋友参考下吧
    2016-05-05
  • ahooks解决用户多次提交方法示例

    ahooks解决用户多次提交方法示例

    这篇文章主要为大家介绍了ahooks解决用户多次提交的方法示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • React项目中动态插入HTML内容的实现

    React项目中动态插入HTML内容的实现

    本文主要介绍了React项目中动态插入HTML内容的实现,通过使用React的dangerouslySetInnerHTML属性,我们可以将HTML内容插入到组件中,具有一定的参考价值,感兴趣的可以了解一下
    2023-10-10

最新评论