当前位置: 首页 > Web前端 > HTML

精读《React useEvent RFC》

时间:2023-03-28 15:44:04 HTML

useEvent解决一个问题:如何保持函数引用不变,同时获取最新状态。本周,我们结合RFC原文和文章WhattheuseEventReacthookis(andisn't)一文的解读来了解这个提案。借用提案中的代码,可以清楚的解释useEvent是什么:functionChat(){const[text,setText]=useState('');//?始终使用相同的函数(即使`text`发生变化)constonClick=useEvent(()=>{sendMessage(text);});return;}onClick保持引用不变,并在每次触发时访问最新的文本值。为什么要提供这个功能,解决什么问题?总览中慢慢说吧。概述定义一个访问最新状态的函数并不难:functionApp(){const[count,setCount]=useState(0)constsayCount=()=>{console.log(count)}return}但sayCount函数引用每次都会变化,会直接破坏Child组件的备忘录效果,甚至会引起更严重的连锁反应(当Child组件调用useEffect中的onClick回调时)。为了保持sayCount引用不变,我们需要用useCallback包装它:functionApp(){const[count,setCount]=useState(0)constsayCount=useCallback(()=>{console.log(count)},[count])return}但即便如此,我们也只能保证在count不变的情况下sayCount引用不变。如果想保持sayCount的引用稳定,就需要去掉依赖[count],这样会导致访问的count一直是初始值,逻辑上会导致更大的问题。一个无奈的办法是维护一个countRef,让它的值和count保持同步,在sayCount中访问countRef:functionApp(){const[count,setCount]=useState(0)constcountRef=React.useRef()countRef.current=countconstsayCount=useCallback(()=>{console.log(countRef.current)},[])return}这种代码可以解决问题,但肯定不行推荐,有两个原因:每个value都需要加上匹配的Ref,非常冗余。直接在函数内部同步更新ref不是一个好主意,但是写在useEffect里面太麻烦了。另一种方法是创建自己的钩子,例如useStableCallback,它本质上是这个提案的主角——useEvent:functionApp(){const[count,setCount]=useState(0)constsayCount=useEvent(()=>{console.log(count)})return}所以useEvent的内部实现大概类似于自定义hookuseStableCallback。提案中还给出了可能的实现思路://(!)ApproximatebehaviorfunctionuseEvent(handler){consthandlerRef=useRef(null);//在实际实现中,这将在布局效果之前运行useLayoutEffect(()=>{handlerRef.current=handler;});returnuseCallback((...args)=>{//在实际实现中,如果在渲染期间调用,这将抛出constfn=handlerRef.current;returnfn(...args);},[]);}In其实很容易理解。我们把需求分成两部分:因为要返回一个稳定的引用,所以最后返回的函数必须使用useCallback,并设置依赖数组为[]。要在执行函数时访问最新的值,必须每次都执行最新的函数。因此在Hook中使用了Ref来存放每次接收到的最新的函数引用。当函数执行时,实际执行的是最新的函数。功能参考。注意两个注释,第一个是useLayoutEffect部分实际上是先于layoutEffect执行的,这是为了保证在事件循环中直接消费函数时可以访问旧的Ref值;第二个是在渲染的时候调用会抛出异常,这个是为了避免useEvent函数在渲染的时候被使用,因为不能数据驱动。精读其实useEvent的概念和实现都很简单。让我们谈谈提案中一些有趣的细节。为什么命名为useEvent提案中提到,如果名字完全以函数命名,不管名字的长短,useStableCallback或useCommittedCallback会更合适,这两者都意味着获得稳定的回调函数。但是useEvent是从用户的角度来命名的,即它产生的函数一般用于组件回调函数,而这些回调函数一般都带有“事件特性”,比如onClick、onScroll,所以开发者看到useEvent时,你可以下意识地提醒自己,你正在写一个事件回调,这是非常直观的。(当然,我觉得主要是为了缩短名字,方便记忆。)值不是实时的。useEvent虽然可以获取到最新的值,但是和useCallback获取ref不同。这种差异体现在:functionApp(){const[count,setCount]=useState(0)constsayCount=useEvent(async()=>{console.log(count)awaitwait(1000)console.log(count)})return}await前后的输出值必须相同。在实现上,count值只是调用的一个快照,所以当函数异步等待时,即使外部改变了count,当前函数调用仍然无法获取到最新的count,ref方法就可以了。理解上,为了避免夜长梦多,回调函数尽量不要写成异步的。useEvent救不了残障人士。如果你坚持写像onSomething={cond?handler1:handler2},那么cond改变后,传递下来的函数引用也会改变。这无论如何是useEvent无法避免的,也许解决办法就是Lint和throwerror。事实上,包装cond?handler1:handler2作为一个整体在useEvent中可以解决引用变化的问题,但是除了Lint,没有人能阻止你绕过它。可以用自定义钩子代替useEvent来实现吗?不能。虽然提案中给出了一个近似的解决方案,但实际上存在两个问题:分配ref时,useLayoutEffect的时机还是不够超前。如果在值改变后理解访问函数,就会得到旧值。生成的函数用于渲染,不给出错误信息。综上所述,useEvent显然是给React增加了一个官方的概念。在大幅增加理解成本的同时,也填补了ReactHooks在实践中欠缺的重要部分。不管你喜不喜欢,问题就在那里,我也给了解决方案,很好。讨论地址是:Jingdu《React useEvent RFC》·Issue#415·dt-fe/weekly想参与讨论的请点这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)