作者:卡颂
简介:《React技术揭秘》作者
来源:SegmentFault思否社区
大家好,我卡颂。
我们知道,Hooks使用时存在所谓的闭包陷阱,考虑如下代码:
functionChat(){const[text,setText]=useState();constonClick=useCallback(()={sendMessage(text);},[]);turnSendButtononClick={onClick}/;}
我们期望点击后sendMessage能传递text的最新值。
然而实际上,由于回调函数被useCallback缓存,形成闭包,所以点击的效果始终是sendMessage()。
这就是闭包陷阱。
以上代码的一种解决方式是为useCallback增加依赖项:
constonClick=useCallback(()={sendMessage(text);},[text]);
但是这么做了后,每当依赖项(text)变化,useCallback会返回一个全新的onClick引用,这就失去了useCallback缓存函数引用的作用。
闭包陷阱的出现,加大了Hooks的上手门槛,也让开发者更容易写出有bug的代码。
现在,React官方团队要出手解决这个问题。
useEvent解决方式是引入一个新的原生Hook——useEvent。
他用于定义一个函数,这个函数有个特性:
在组件多次nder时保持引用一致
函数内始终能获取到最新的props与state
上面的例子使用useEvent改造后:
functionChat(){const[text,setText]=useState();constonClick=useEvent(()={sendMessage(text);});turnSendButtononClick={onClick}/;}
在Chat组件多次nder时,onClick始终指向同一个引用。
并且onClick触发时始终能获取到text的最新值。
之所以叫useEvent,是因为React团队认为这个Hook的主要应用场景是:封装事件处理函数。
useEvent的实现useEvent的实现并不困难,代码类似如下:
functionuseEvent(handler){consthandlerRef=useRef(null);//视图渲染完成后更新`handlerRef.curnt`指向useLayoutEffect(()={handlerRef.curnt=handler;});//用useCallback包裹,使得nder时返回的函数引用一致turnuseCallback((...args)={constfn=handlerRef.curnt;turnfn(...args);},[]);}
整体包括两部分:
返回一个没有依赖项的useCallback,使得每次nder时函数的引用一致
useCallback((...args)={constfn=handlerRef.curnt;turnfn(...args);},[]);在合适的时机更新handlerRef.curnt,使得实际执行的函数始终是最新的引用
与开源Hooks的差异很多开源Hooks库已经实现类似功能(比如ahooks中的useMemoizedFn)
useEvent与这些开源实现的差异主要体现在:
useEvent定位于处理事件回调函数这一单一场景,而useMemoizedFn定位于缓存各种函数。
那么问题来了,既然功能类似,那useEvent为什么要限制自己的使用场景呢?
答案是:为了更稳定。
useEvent能否获取到最新的state与props取决于handlerRef.curnt更新的时机。
在上面模拟实现中,useEvent更新handlerRef.curnt的逻辑放在useLayoutEffect回调中进行。
这就保证了handlerRef.curnt始终在视图完成渲染后再更新:
useLayoutEffect(()={handlerRef.curnt=handler;});
而事件回调触发的时机显然在视图完成渲染之后,所以能够稳定获取到最新的state与props。
注:源码内的实际更新时机会更早些,但不影响这里的结论
再来看看ahooks中的useMemoizedFn,fnRef.curnt的更新时机是useMemoizedFn执行时(即组件nder时):
functionuseMemoizedFnTextendsnoop(fn:T){constfnRef=useRefT(fn);//更新fnRef.curntfnRef.curnt=useMemo(()=fn,[fn]);//...省略代码}
当React18启用并发更新后,组件nder的次数、时机并不确定。
所以useMemoizedFn中fnRef.curnt的更新时机也是不确定的。
这就增加了在并发更新下使用时潜在的风险。
可以说,useEvent通过限制handlerRef.curnt更新时机,进而限制应用场景,最终达到稳定的目的。
总结useEvent当前还处于RFC(RequestForComments)阶段。很多热心的开发者对这个Hook的命名提出了建议,比如:
useStableCallback:
又比如:useLatestClosu:
从这些命名看,他们显然扩大了useEvent的应用场景。
经过本文的分析我们知道,扩大应用场景意味着增加开发者使用时出错的风险。
点击左下角阅读原文,到SegmentFault思否社区和文章作者展开更多互动和交流,扫描下方”