当前位置: 首页 > 科技观察

Ahooks是如何解决React的闭包问题的?_0

时间:2023-03-19 19:29:01 科技观察

这篇文章探讨了ahooks是如何解决React的闭包问题的?我们来看一个React的闭包问题的例子:importReact,{useState,useEffect}from"react";exportdefault()=>{const[count,setCount]=useState(0);useEffect(()=>{setInterval(()=>{console.log("setInterval:",count);},1000);},[]);return(

count:{count}
setCount((val)=>val+1)}>增加1
);};代码示例[4]当我点击按钮时,发现setInterval中打印的值没有变化,一直是0。这就是React的闭包问题。这样做的原因是为了维护FunctionComponent的状态,React使用一个链表来存储FunctionComponent中的hook,并为每个hook创建一个对象。const[count,setCount]=useState(0);useEffect(()=>{setInterval(()=>{console.log("setInterval:",count);},1000);},[]);this对象的memoizedState属性用于存储组件上次更新后的状态,next指向下一个钩子对象。在组件更新过程中,hooks函数的执行顺序不变,根据这个链表可以得到当前hooks对应的Hook对象。这就是功能组件具有状态能力的方式。同时制定了一系列的规则,比如hook不能写入if...else...等,从而保证能够正确获取对应hook的状态。useEffect接收两个参数,一个回调函数和一个数组。数组里面是useEffect的依赖。当为[]时,回调函数只会在组件第一次渲染时执行一次。如果有其他依赖,react会判断它的依赖是否发生了变化,如果发生变化,则执行回调函数。回到刚才的例子://解决方案一useEffect(()=>{if(timer.current){clearInterval(timer.current);}timer.current=setInterval(()=>{console.log("setInterval:",计数);},1000);},[计数]);第一次执行时执行useState,计数为0。执行useEffect,执行其回调中的逻辑,启动定时器,每隔1s输出setInterval:0。当我点击按钮将计数增加1时,整个功能组件被重新渲染。这时候之前执行的链表已经存在了。useState将Hook对象上保存的state设置为1,那么此时count也为1。使用空依赖项和无回调函数执行useEffect。但是之前的回调函数还在,还是会执行console.log("setInterval:",count);每隔1s一次,但是这里的count是之前第一次执行时的count值,因为在timer的回调中引用了函数,形成闭包并保持。解决方案方案一:为useEffect设置依赖,重新执行函数,设置新的定时器,获取最新的值。const[count,setCount]=useState(0);useEffect(()=>{setInterval(()=>{console.log("setInterval:",count);},1000);},[]);解决方法二:使用useRef。useRef返回一个可变的ref对象,其.current属性被初始化为传递的参数(initialValue)。useRef创建一个普通的Javascript对象,并在每次渲染时返回相同的ref对象。当我们更改它的当前属性时,对象的引用是相同的,所以可以在定时器中读取最新的值。constlastCount=useRef(count);//解决方案2useEffect(()=>{setInterval(()=>{console.log("setInterval:",lastCount.current);},1000);},[]);return(
count:{count}
{setCount((val)=>val+1);//+1lastCount.current+=1;}}>增加1
);useRef=>useLatest终于回到我们的ahooks主题,基于上面第二种方案,useLatesthook诞生了。返回当前最新值的Hook,可以避免闭包问题。实现原理很简单,短短十行代码,就是用useRef包裹一层:import{useRef}from'react';//通过useRef,保留每次获取的最新值functionuseLatest(值:T){constref=useRef(值);ref.current=值;returnref;}exportdefaultuseLatest;useEvent=>useMemoizedFnReact另一个场景是基于useCallback。const[count,setCount]=useState(0);constcallbackFn=useCallback(()=>{console.log(`当前计数为${count}`);},[]);不管上面的情况,我们的count值变化了多少,执行callbackFn打印出来的count的值一直是0。这是因为回调函数被useCallback缓存起来形成了闭包,从而形成了闭包陷阱。那么我们如何解决这个问题呢?官方提出useEvent。它解决的问题是:如何在保持函数引用不变的同时获取最新的状态。使用后,上面的例子就变成了。constcallbackFn=useEvent(()=>{console.log(`当前计数为${count}`);});这里我们不细看这个特性,其实在ahooks中已经实现了一个类似的功能,那就是useMemoizedFn。useMemoizedFn是持久函数的Hook。理论上可以用useMemoizedFn来完全替代useCallback。使用useMemoizedFn,第二个参数deps可以省略,同时保证函数地址??永不改变。上面的问题可以通过下面的方法轻松解决:constmemoizedFn=useMemoizedFn(()=>{console.log(`当前计数为${count}`);});Demo地址[5]来看看它的源码,可以看到它仍然通过useRef保持函数引用地址不变,每次执行都能拿到最新的状态值。functionuseMemoizedFn(fn:T){//通过useRef保持其引用地址不变,value可以保持最新值constfnRef=useRef(fn);fnRef.current=useMemo(()=>fn,[fn]);//通过useRef保持其引用地址不变,value可以保持最新值constmemoizedFn=useRef>();if(!memoizedFn.current){//返回的Persistent函数,调用此函数时,调用原函数memoizedFn.current=function(this,...args){returnfnRef.current.apply(this,args);};}将memoizedFn.current作为T返回;}总结与思考自从React引入hooks后,虽然解决了类组件的一些缺点,比如逻辑复用需要通过high-levelstages进行嵌套。但是它也引入了一些问题,比如闭包问题。这是由React的FunctionComponentStatemanagement引起的,这有时会让开发人员感到困惑。开发人员可以通过添加依赖项或使用useRef来避免这种情况。ahooks也意识到了这个问题,通过使用useLatest确保获取到最新值,使用MemoizedFn持久化函数来避免类似的闭包陷阱。值得一提的是,useMemoizedFn是ahooks输出函数的标准,所有的输出函数都使用useMemoizedFn包裹一层。另外,所有的输入函数都使用useRef做记录,保证随时随地都能访问到最新的函数。供参考,从reacthooks的“闭包陷阱”说起,说说reacthooks[6]React官方团队出手弥补原生Hooks的不足[7]参考资料[1]详情:https://github.com/GpingFeng/hooks[2]大家看得懂的源码(一)ahooks整体结构:https://juejin.cn/post/7105396478268407815[3]如何使用插件机制优雅封装你的请求hook:https://juejin.cn/post/7105396478268407815juejin.cn/post/7105733829972721677[4]代码示例:https://codesandbox.io/s/ji-chu-yong-fa-forked-wpk1s6?file=/App.tsx:0-487[5]演示地址:https://codesandbox.io/s/ji-chu-yong-fa-forked-7pkp1r?file=/App.tsx[6]从ReactHooks的“闭包陷阱”,说说ReactHooks:https://juejin.cn/post/6844904193044512782[7]React官方团队出手弥补原生Hook的不足:https://segmentfault.com/a/1190000041798153