本文不会过多讲解基础知识,更多说的是在使用useRef如何能摆脱 这个 “闭包陷阱” ?
react hooks 的“闭包陷阱” 基本每个开发员都有遇见,这是很令人抓狂的。
(以下react示范demo,均为react 16.8.3 版本)
列一个具体的场景:
function App(){ const [count, setCount] = useState(1); useEffect(()=>{ setInterval(()=>{ console.log(count) }, 1000) }, []) }
其实结果都回事1。不相信就可以看。咋样是不是很懵。我们先浅谈一些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的 Fiber 架构,其实一个 Fiber节点就对应的是一个组件。对于classComponent而言,有state是一件很正常的事情,Fiber对象上有一个memoizedState用于存放组件的state。
ok,现在看 hooks 所针对的FunctionComponnet。 无论开发者怎么折腾,一个对象都只能有一个state属性或者memoizedState属性,可是,谁知道可爱的开发者们会在FunctionComponent里写上多少个useState,useEffect等等 ? 所以,react用了链表这种数据结构来存储FunctionComponent里面的 hooks。比如:
function App(){ const [count, setCount] = useState(1) const [name, setName] = useState('chechengyi') useEffect(()=>{ }, []) const text = useMemo(()=>{ return 'ffffd' }, []) }
在组件第一次渲染的时候,为每个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(()=>{ setInterval(()=>{ console.log(count) }, 1000) }, []) function click(){ setCount(2) } }
现在构建,组件第一次渲染执行App(),执行useState设置了初始状态为1,所以此时的count为1。然后执行了useEffect,回调函数执行,设置了一个定时器每隔 1s 打印一次count。
当click函数被触发了,调用setCount(2)肯定会触发react的更新,更新到当前组件的时候也是执行App(),之前说的链表已经形成了哈,此时useState将Hook对象 上保存的状态置为2, 那么此时count也为2了。然后在执行useEffect由于依赖数组是一个空的数组,所以此时回调并不会被执行。
但又有一个新问题。这次更新的过程中根本就没有涉及到这个定时器,这个定时器还在坚持的,默默的,每隔1s打印一次count。 注意这里打印的count,是组件第一次渲染的时候App()时的count,count的值为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能够每次拿到新鲜的值?
看看下面代码:
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 更是一种总结,掌握知识、项目经验都是缺一不可的。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/128260.html
摘要:理解的函数基础要搞好深入浅出原型使用原型模型,虽然这经常被当作缺点提及,但是只要善于运用,其实基于原型的继承模型比传统的类继承还要强大。中文指南基本操作指南二继续熟悉的几对方法,包括,,。商业转载请联系作者获得授权,非商业转载请注明出处。 怎样使用 this 因为本人属于伪前端,因此文中只看懂了 8 成左右,希望能够给大家带来帮助....(据说是阿里的前端妹子写的) this 的值到底...
摘要:欢迎来我的个人站点性能优化其他优化浏览器关键渲染路径开启性能优化之旅高性能滚动及页面渲染优化理论写法对压缩率的影响唯快不破应用的个优化步骤进阶鹅厂大神用直出实现网页瞬开缓存网页性能管理详解写给后端程序员的缓存原理介绍年底补课缓存机制优化动 欢迎来我的个人站点 性能优化 其他 优化浏览器关键渲染路径 - 开启性能优化之旅 高性能滚动 scroll 及页面渲染优化 理论 | HTML写法...
阅读 507·2023-03-27 18:33
阅读 709·2023-03-26 17:27
阅读 607·2023-03-26 17:14
阅读 580·2023-03-17 21:13
阅读 505·2023-03-17 08:28
阅读 1757·2023-02-27 22:32
阅读 1265·2023-02-27 22:27
阅读 2107·2023-01-20 08:28