陷进到处都是啊!本篇文章就说说Hooks使用时存在所谓的闭包陷阱,看看下面代码:
function Chat() { const [text, setText] = useState(''); const onClick = useCallback(() => { sendMessage(text); }, []); return <SendButton onClick={onClick} />; }
想要实现的是点击后sendMessage能传递text的最新值。
可却无法实现,就是由于回调函数被useCallback缓存,形成闭包,所以点击的效果始终是sendMessage('')。
这就是闭包陷阱。
以上代码的一种解决方式是为useCallback增加依赖项:
const onClick = useCallback(() => { sendMessage(text); }, [
text]);
上面操作之后,每当依赖项(text)变化,useCallback会返回一个全新的onClick引用,这就失去了useCallback缓存函数引用的作用。
闭包陷阱的出现,加大了Hooks的上手门槛,这样的bug在代码中很容易发现。
看看React官方团队解决这个问题。
useEvent
解决方式是引入一个新的原生Hook——useEvent。
他用于定义一个函数,这个函数有2个特性:
在组件多次render时保持引用一致
函数内始终能获取到最新的props与state
上面的例子使用useEvent改造后:
function Chat() { const [text, setText] = useState(''); const onClick = useEvent(() => { sendMessage(text); }); return <SendButton onClick={onClick} />; }
在Chat组件多次render时,onClick始终指向同一个引用。
并且onClick触发时始终能获取到text的最新值。
之所以叫useEvent,是因为React团队认为这个Hook的主要应用场景是:封装事件处理函数。
useEvent的实现
useEvent的实现并不困难,代码类似如下:
function useEvent(handler) { const handlerRef = useRef(null); // 视图渲染完成后更新`handlerRef.current`指向 useLayoutEffect(() => { handlerRef.current = handler; }); // 用useCallback包裹,使得render时返回的函数引用一致 return useCallback((...args) => { const fn = handlerRef.current; return fn(...args); }, []); }
整体包括两部分:
返回一个没有依赖项的useCallback,使得每次render时函数的引用一致
useCallback((...args) => { const fn = handlerRef.current; return fn(...args); }, []);
在合适的时机更新handlerRef.current,使得实际执行的函数始终是最新的引用
与开源Hooks的差异
要知道现在很多开源Hooks库已经实现类似功能(比如ahooks中的useMemoizedFn)
useEvent与这些开源实现的差异主要体现在:
useEvent定位于处理事件回调函数这一单一场景,而useMemoizedFn定位于缓存各种函数。
那么问题来了,既然功能类似,那useEvent为什么要限制自己的使用场景呢?
答案是:为了更稳定。
useEvent能否获取到最新的state与props取决于handlerRef.current更新的时机。
在上面模拟实现中,useEvent更新handlerRef.current的逻辑放在useLayoutEffect回调中进行。
这就保证了handlerRef.current始终在视图完成渲染后再更新:
useLayoutEffect(() => { handlerRef.current = handler; });
而事件回调触发的时机显然在视图完成渲染之后,所以能够稳定获取到最新的state与props。
注:源码内的实际更新时机会更早些,但不影响这里的结论
再来看看ahooks中的useMemoizedFn,fnRef.current的更新时机是useMemoizedFn执行时(即组件render时):
function useMemoizedFn<T extends noop>(fn: T) { const fnRef = useRef<T>(fn); // 更新fnRef.current fnRef.current = useMemo(() => fn, [fn]); // ...省略代码 }
当React18启用并发更新后,组件render的次数、时机并不确定。
所以useMemoizedFn中fnRef.current的更新时机也是不确定的。
这就增加了在并发更新下使用时潜在的风险。
可以说,useEvent通过限制handlerRef.current更新时机,进而限制应用场景,最终达到稳定的目的。
总结
useEvent当前还处于RFC(Request For Comments)阶段。
很多热心的开发者对这个Hook的命名提出了建议,比如:useStableCallback:
又比如:useLatestClosure:
上面其实就是要扩大了useEvent的应用场景。
但扩大后应用场景意味着增加开发者使用时出错的风险,这是不可行的。希望大家多多学习。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/128258.html
本文不会过多讲解基础知识,更多说的是在使用useRef如何能摆脱 这个 闭包陷阱 ? react hooks 的闭包陷阱 基本每个开发员都有遇见,这是很令人抓狂的。 (以下react示范demo,均为react 16.8.3 版本) 列一个具体的场景: functionApp(){ const[count,setCount]=useState(1); useEffect(()=...
想必大家都能看得懂的源码 ahooks 整体架构篇,且可以使用插件化机制优雅的封装你的请求hook,现在我们就探讨下ahooks 是怎么解决 React 的闭包问题的?。 React 的闭包问题 先来看一个例子: importReact,{useState,useEffect}from"react"; exportdefault()=>{ const[c...
摘要:比如就是一种,它可以用来管理状态返回的结果是数组,数组的第一项是值,第二项是赋值函数,函数的第一个参数就是默认值,也支持回调函数。而之所以输出还是正确的,原因是的回调函数中,值永远指向最新的值,因此没有逻辑漏洞。 1. 引言 如果你在使用 React 16,可以尝试 Function Component 风格,享受更大的灵活性。但在尝试之前,最好先阅读本文,对 Function Com...
摘要:当组件安装和更新时,回调函数都会被调用。好在为我们提供了第二个参数,如果第二个参数传入一个数组,仅当重新渲染时数组中的值发生改变时,中的回调函数才会执行。 前言 首先欢迎大家关注我的Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励,希望大家多多关注呀!React 16.8中新增了Hooks特性,并且在React官方文档中新增...
摘要:对于所访问的每个元素,函数应该将该元素传递给所提供的回调函数。 HTML 在线阅读Github地址 题目列表 HTML HTML和XHTML的区别 Html的语义化 Doctype的文档类型 cookie、sessionSttorage、localStory区别 HTML全局属性(global attribute)有哪些? 常见的浏览器内核有哪些? 介绍一下你对浏览器内核的理解?...
阅读 498·2023-03-27 18:33
阅读 706·2023-03-26 17:27
阅读 606·2023-03-26 17:14
阅读 575·2023-03-17 21:13
阅读 498·2023-03-17 08:28
阅读 1753·2023-02-27 22:32
阅读 1259·2023-02-27 22:27
阅读 2065·2023-01-20 08:28