最新要闻
- 每日观点:当日快讯:沙特外交大臣表示阿拉伯国家将继续在俄乌冲突中维持中立
- 环球短讯!近视可用 特步0~700度专业大框泳镜狂促:12元包邮
- 店员称衣服掉地上摔个洞需赔款:最终结果让网友不淡定 速看
- 如何保存新鲜活虾 活虾怎么保存? 全球即时看
- 全球微动态丨体验阿斯顿·马丁DBX707 看看超跑品牌是怎么做SUV的
- 比亚迪首家全品牌体验中心开业:几万块到一百万的车全都有 当前简讯
- 第一批升级iOS 16.5正式版的用户被坑了!_要闻速递
- 又有基金公司宣布:APP停止运营!_环球快看
- 清华大学女生获选美冠军 网友:全方位优秀
- 年轻人第一辆后驱SUV 长安深蓝S3预售:16.99万起-热议
- 全球信息:雨前高山春鲜 谢裕大珍珠绿茶60克到手29元
- 万达集团紧急声明!
- 热搜第一!BLG晋级 LPL提前锁定MSI三连冠 每日信息
- 全面了解华为全屋智能4.0:体验质变 最新快讯
- 母亲5点帮女儿排队领证结果走错地方:白排了一个小时队
- 当前快看:河海大学与华中师范大学签署战略合作协议
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
React闭包陷阱
React闭包陷阱
React Hooks
是React 16.8
引入的一个新特性,其出现让React
的函数组件也能够拥有状态和生命周期方法,其优势在于可以让我们在不编写类组件的情况下,更细粒度地复用状态逻辑和副作用代码,但是同时也带来了额外的心智负担,闭包陷阱就是其中之一。
闭包
从React
闭包陷阱的名字就可以看出来,我们的问题与闭包引起的,那么闭包就是我们必须要探讨的问题了。函数和对其词法环境lexical environment
的引用捆绑在一起构成闭包,也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript
,函数在每次创建时生成闭包。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。通常来说,一段程序代码中所用到的名字并不总是有效或可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域scope
,当一个方法或成员被声明,他就拥有当前的执行上下文context
环境,在有具体值的context
中,表达式是可见也都能够被引用,如果一个变量或者其他表达式不在当前的作用域,则将无法使用。作用域也可以根据代码层次分层,以便子作用域可以访问父作用域,通常是指沿着链式的作用域链查找,而不能从父作用域引用子作用域中的变量和引用。
(资料图片)
为了定义一个闭包,首先需要一个函数来套一个匿名函数。闭包是需要使用局部变量的,定义使用全局变量就失去了使用闭包的意义,最外层定义的函数可实现局部作用域从而定义局部变量,函数外部无法直接访问内部定义的变量。从下边这个例子中我们可以看到定义在函数内部的name
变量并没有被销毁,我们仍然可以在外部使用函数访问这个局部变量,使用闭包,可以把局部变量驻留在内存中,从而避免使用全局变量,因为全局变量污染会导致应用程序不可预测性,每个模块都可调用必将引来灾难。
const Student = () => { const name = "Ming"; const sayMyName = function(){ // `sayMyName`作为内部函数,有权访问父级函数作用域`Student`中的变量 console.log(name); } console.dir(sayMyName); // ... `[[Scopes]]: Scopes[2] 0: Closure (student) {name: "Ming"} 1: Global` ... return sayMyName; // `return`是为了让外部能访问闭包,挂载到`window`对象实际效果是一样的}const stu = Student(); stu(); // `Ming`
实际开发中使用闭包的场景有非常多,例如我们常常使用的回调函数。回调函数就是一个典型的闭包,回调函数可以访问父级函数作用域中的变量,而不需要将变量作为参数传递到回调函数中,这样就可以减少参数的传递,提高代码的可读性。在下边这个例子中,我们可以看到local
这个变量是局部的变量,setTimeout
进行调用的词法作用域是全局的作用域,理论上是无法访问local
这个局部变量的,但是我们采用了闭包的方式创建了一个能够访问内部局部变量的函数,所以这个变量的值能够被正常打印。如果我们类似于第二个setTimeout
直接将参数传递也是可以的,但是如果我们在这里封装了很多逻辑,那么这个参数传递就变得比较复杂了,根据实际情况用闭包可能会更合适一些。
const cb = () => { const local = 1; return () => { console.log(local); };}setTimeout(cb(), 1000); // 1setTimeout(console.log, 2000, 2); // 2
我们可以再看一个例子,我们在写Node
时可能会遇到一个场景,在调用其他第三方服务接口的时候会会被限制频率,比如对于该接口1s
最多请求3
次,此时我们通常有两种解决方案,一种方案是在请求的时候就限制发起请求的频率,直接在发起的时候就控制好,被限频的请求需要排队,另一种方案是不限制发起请求的频率,而是采用一种基于重试的机制,当请求的结果是被限频的时候,我们就延迟一段时间再次发起请求,可以用指数退避算法等方式来控制重试时间,实际上以太网在拥堵的时候就采用了这种方法,每次发生碰撞后,设备会根据指数退避算法来计算等待时间,等待时间会逐渐增加,从而降低了设备再次发生碰撞的概率。
在这里我们需要关注第二种方案中如何进行重试,我们在发起请求的时候通常会携带比较多的信息,比如url
、token
、body
等数据进行查询,如果我们需要进行重试,那么肯定需要找个地方把这些数据存储下来以备下次发起请求,那么在何处存储这些变量呢,当然我们可以在global/window
中构造一个全局的对象来存储,但是之前也提到过了全局变量污染会导致应用程序不可预测性,所以在这里我们更希望用闭包来进行存储。在下边这个例子中我们就使用了闭包来存储了请求时的一些信息,并且在重试时保证了这些信息是最初定义时的信息,这样就不需要污染全局变量,而且需要对于业务调用来说,我们可以再包装一侧requestWithLimit
,当内部的请求正常完整之后才会Resolve Promise
,将这部分重试机制封装到内部会更加易用。
const requestFactory = (url, token) => { return function request(){ // 假设这个函数会发起请求并且返回结果 return { url, token }; }}const req1 = requestFactory("url1", "token1");console.log(req1()); // 发起请求 `{url: "url1", token: "token1"}`console.log(req1()); // 重试请求 `{url: "url1", token: "token1"}`const req2 = requestFactory("url2", "token2");console.log(req2()); // 发起请求 `{url: "url2", token: "token2"}`console.log(req2()); // 重试请求 `{url: "url2", token: "token2"}`
Js
是静态作用域,但是this
对象却是个例外,this
的指向问题就类似于动态作用域,其并不关心函数和作用域是如何声明以及在何处声明的,只关心是从何处调用的,this
的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this
到底指向谁,当然实际上this
的最终指向的是那个调用的对象。this
的设计主要是为了能够在函数体内部获得当前的运行环境context
,因为在Js
的内存设计中Function
是独立的一个堆地址空间,不和Object
直接相关,所以才需要绑定一个运行环境。
前边提到了词法作用域是在定义时就确定了,所以词法作用域也可以称为静态作用域。那么我们可以看下下边的例子,这个例子是不是很像我们的React Hooks
来定义的组件。运行这个例子之后,我们可以看到虽然对于这个函数执行起来看起来都是是完全一样的,但是最后打印的时候得到的值是得到了之前作用域中的值。我们现在需要关注的是fn
这个函数,我们我们说的定义时确定词法作用域这句话具体指的是这个函数被声明并定义的时候确定词法作用域,或者说是在生成函数地址的时候确定词法作用域。其实但从这个例子看起来好像没什么问题,本来就是应该这个样子的,那么为什么要举这个例子呢,其实在这里想表达的意思是,如果我们在写代码的时候不小心保持了之前的fn
函数地址,那么虽然我们希望得到的index
是5
,但是实际拿到的index
却是1
,这其实就是所谓的闭包陷阱了,我们在下边探讨React
的时候也可以通过这个例子理解React
的视图模型。
const collect = [];const View = (props) => { const index = props.index; const fn = () => { console.log(index); } collect.push(fn); return index;}for(let i=0; i<5; ++i){ View({index: i + 1});}collect.forEach(fn => fn()); // 1 2 3 4 5
闭包陷阱
说到这陷阱,不由得想起来一句话,出门出门就上当,当当当当不一样,平时开发的时候可以说是一不小心就上当掉入了陷阱。那么我们这个陷阱是完全由闭包引起的吗,那肯定不是,这只是Js
的语言特性而已,那么这个陷阱是完全由React
引起的吗,当然也不是,所以接下来我们就要来看看为什么需要闭包和React
结合会引发这个陷阱。
首先我们要考虑下React
渲染视图的机制,我们可以想一下,React
是没有模版的,类似于Vue
的template
这部分,那么也就是说React
是很难去拿到我们希望渲染的视图,就更不用谈去做分析了。那么在Hooks
中应该如何拿到视图再去更新DOM
结构呢,很明显我们实际上只需要将这个Hooks
执行一遍即可,无论你定义了多少分支多少条件,我只要执行一遍最后取得返回值不就可以拿到视图了嘛。同时也是因为React
渲染视图非常的灵活,从而不得不这样搞,Vue
不那么灵活但是因为模版的存在可以做更多的优化,这实际上还是个取舍问题。不过这不是我们讨论的重点,既然我们了解到了React
的渲染机制,而且在上边我们举了一个函数多次运行的示例,那么在这里我们举一个组件多次执行的示例,
// https://codesandbox.io/s/react-closure-trap-jl9jos?file=/src/multi-count.tsximport React, { useState } from "react";const collect: (() => number)[] = [];export const MultiCount: React.FC = () => { const [count, setCount] = useState(0); const click = () => { setCount(count + 1); }; collect.push(() => count); const logCollect = () => { collect.forEach((fn) => console.log(fn())); }; return ( {count} );};
我们首先点击三次count++
这个按钮,此时我们的视图上的内容是3
,但是此时我们点击log >> count
这个按钮的时候,发现在控制台打印的内容是0 1 2 3
,这其实就是跟前边的例子一样,因为闭包+
函数的多次执行造成的问题,因为实际上Hooks
实际上无非就是个函数,React
通过内置的use
为函数赋予了特殊的意义,使得其能够访问Fiber
从而做到数据与节点相互绑定,那么既然是一个函数,并且在setState
的时候还会重新执行,那么在重新执行的时候,点击按钮之前的add
函数地址与点击按钮之后的add
函数地址是不同的,因为这个函数实际上是被重新定义了一遍,只不过名字相同而已,从而其生成的静态作用域是不同的,那么这样便可能会造成所谓的闭包陷阱。
其实关于闭包陷阱的问题,大部分都是由于依赖更新不及时导致的,例如useEffect
、useCallback
的依赖定义的不合适,导致函数内部保持了对上一次组件刷新时定义的作用域,从而导致了问题。例如下边这个例子,我们的useEffect
绑定的事件依赖是count
,但是我们在点击count++
的时候,实际上useEffect
要执行的函数并没有更新,所以其内部的函数依然保持了上一次的作用域,从而导致了问题。
// https://codesandbox.io/s/react-closure-trap-jl9jos?file=/src/bind-event.tsximport { useEffect, useRef, useState } from "react";export const BindEventCount: React.FC = () => { const ref1 = useRef(null); const [count, setCount] = useState(0); const add = () => { setCount(count + 1); }; useEffect(() => { const el = ref1.current; const handler = () => console.log(count); el?.addEventListener("click", handler); return () => { el?.removeEventListener("click", handler); }; }, []); return ( {count} );};
当我们多次点击count++
按钮之后,再去点击log count 1
按钮,发现控制台输出的内容还是0
,这就是因为我们的useEffect
保持了旧的函数作用域,而那个函数作用的count
为0
,那么打印的值当然就是0
,同样的useCallback
也会出现类似的问题,解决这个问题的一个简单的办法就是在依赖数组中加入count
变量,当count
发生变化的时候,就会重新执行useEffect
,从而更新函数作用域。那么问题来了,这样就能解决所有问题吗,显然是不能的,副作用依赖可能会造成非常长的函数依赖,可能会导致整个项目变得越来越难以维护,关于事件绑定的探讨可以研究下前边 Hooks
与事件绑定 这篇文章。
那么有没有什么好办法解决这个问题,那么我们就需要老朋友useRef
了,useRef
是解决闭包问题的万金油,其能存储一个不变的引用值。设想一下我们只是因为读取了旧的作用域中的内容而导致了问题,如果我们能够得到一个对象使得其无论更新了几次作用域,我们都能够保持对同一个对象的引用,那么更新之后直接取得这个值不就可以解决这个问题了嘛。在React
中我们就可以借助useRef
来做到这点,通过保持对象的引用来解决上述的问题。
// https://codesandbox.io/s/react-closure-trap-jl9jos?file=/src/use-ref.tsximport { useEffect, useRef, useState } from "react";export const RefCount: React.FC = () => { const ref1 = useRef(null); const [count, setCount] = useState(0); const refCount = useRef(count); const add = () => { setCount(count + 1); }; refCount.current = count; useEffect(() => { const el = ref1.current; const handler = () => console.log(refCount.current); el?.addEventListener("click", handler); return () => { el?.removeEventListener("click", handler); }; }, []); return ( {count} );};
同样的,当我们多次点击count++
按钮之后,再去点击log count 1
按钮,发现控制台输出的内容就是最新的count
值了而不是跟上边的例子一样一直保持0
,这就是通过在Hooks
中保持了同一个对象的引用而实现的。通过useRef
我们就可以封装自定义Hooks
来完成相关的实现,例如有必要的话可以实现一个useRefState
,将state
和ref
一并返回,按需取用。再比如下边这个ahooks
实现的useMemoizedFn
,第一个ref
保证永远是同一个引用,也就是说返回的函数永远指向同一个函数地址,第二个ref
用来保存当前传入的函数,这样发生re-render
的时候每次创建新的函数我们都将其更新,也就是说我们即将调用的永远都是最新的那个函数。由此通过两个ref
我们就可以保证两点,第一点是无论发生多少次re-render
,我们返回的都是同一个函数地址,第二点是无论发生了多少次re-render
,我们即将调用的函数都是最新的。
type noop = (this: any, ...args: any[]) => any;type PickFunction = ( this: ThisParameterType, ...args: Parameters) => ReturnType;function useMemoizedFn(fn: T) { const fnRef = useRef(fn); // why not write `fnRef.current = fn`? // https://github.com/alibaba/hooks/issues/728 fnRef.current = useMemo(() => fn, [fn]); const memoizedFn = useRef>(); if (!memoizedFn.current) { memoizedFn.current = function (this, ...args) { return fnRef.current.apply(this, args); }; } return memoizedFn.current as T;}
每日一题
https://github.com/WindrunnerMax/EveryDay
参考
https://juejin.cn/post/6844904193044512782https://juejin.cn/post/7119839372593070094http://www.ferecord.com/react-hooks-closure-traps-problem.html
关键词:
React闭包陷阱
每日观点:当日快讯:沙特外交大臣表示阿拉伯国家将继续在俄乌冲突中维持中立
全球快报:【财经分析】全球贸易增长正在恢复 应继续加强多边贸易合作
环球短讯!近视可用 特步0~700度专业大框泳镜狂促:12元包邮
店员称衣服掉地上摔个洞需赔款:最终结果让网友不淡定 速看
如何保存新鲜活虾 活虾怎么保存? 全球即时看
全球微动态丨体验阿斯顿·马丁DBX707 看看超跑品牌是怎么做SUV的
比亚迪首家全品牌体验中心开业:几万块到一百万的车全都有 当前简讯
第一批升级iOS 16.5正式版的用户被坑了!_要闻速递
学系统集成项目管理工程师(中项)系列23b_信息系统集成及服务管理(下)
【爬虫数据集】滇西小哥YouTube频道TOP10热门视频的热评数据,共2W条!_世界速看料
又有基金公司宣布:APP停止运营!_环球快看
清华大学女生获选美冠军 网友:全方位优秀
年轻人第一辆后驱SUV 长安深蓝S3预售:16.99万起-热议
全球信息:雨前高山春鲜 谢裕大珍珠绿茶60克到手29元
万达集团紧急声明!
03-点亮LED灯
热搜第一!BLG晋级 LPL提前锁定MSI三连冠 每日信息
全面了解华为全屋智能4.0:体验质变 最新快讯
母亲5点帮女儿排队领证结果走错地方:白排了一个小时队
当前快看:河海大学与华中师范大学签署战略合作协议
无论多少次,还是会为粉色疯狂心动!!!
文心一言 VS 讯飞星火 VS chatgpt (19)-- go语言的slice和rust语言的Vec的扩容流程是什么? 世界热文
阴阳师×GARNiDELiA决定展开合作 或为《极乐净土》高清重制版
索尼宣布将与WNBA达成合作伙伴关系 探索各种营销机会
环球今亮点!年终工作总结会议流程_年终工作总结会议通知
NASA发出警告称巨型小行星正在接近地球 大小为纽约地标自由女神像的两倍
网易旗下Jackalope Games宣布更名 正在开发一部基于“战锤”IP的游戏
苹果App商城现已有178万个应用程序 为有意义的统计数据
,影片《人生路不熟》票房突破9亿元大关 由马丽与乔杉等人主演
电视剧《仙剑奇侠传六》官微发布新海报 由许凯和虞书欣担任
顺丰控股公布4月快递物流业务经营 业务量同比增长29.59%
差价上千元该怎么选?13代酷睿i5和i7实测性能对比
华为把屏幕边框做到了1mm!畅享60 Pro即将开卖:1499元
国网东营供电公司开展全市电力设施和电能保护集中宣传活动
从350nm升到4nm 25年来AMD CPU性能已提升910倍
女子动车上提醒男子小声打电话被怼:你凭什么说我
520当天一女子逼停汽车泼粥骂渣男引围观:网友吐槽不该糟蹋粮食
由于半导体消费持续低迷 三星半导体将推迟平泽工厂4nm生产线设备投资
2023年DPC中国联赛夏季赛开战 Aster战队首秀大放异彩赢得首胜
阅读笔记:Sybilla DLT任务重启判定系统 焦点快看
首款天玑9200旗舰!vivo X90降价:256GB版3799元到手
网红三千哥直播PK饮酒过量去世 友人:喝了至少四瓶|当前通讯
英伟达RTX 4060 Ti显卡降临:关键参数已曝光 就差价格了
初三下册月考复习:第二单元知识点-天天快播报
Java生成二维码及条形码工具
python中的装饰器原理和作用 焦点热议
23款奔驰GLC评测_全球观焦点
有你家吗?杭州人均存款达16万元:全国住户存款最强8大城市 每日消息
华为笔记本性能怪兽!MateBook 16s下周首销:i7+1TB仅7999元 全球即时看
今日精选:20年来x86巨变 Intel将精简CPU架构:转向纯血64位
女子为骗男友结婚定制美颜假身份证引热议:网友感慨太假太美了 焦点报道
情侣筷子卖22元单身筷1元 超市:已下架-环球看点
双鸭山市气象台发布大风蓝色预警【IV级/一般】【2023-05-20】 全球热文
每日看点!Natasha 插件化之dll
天天最资讯丨暴雪又搞砸了 老外批《守望先锋2》太失败:浪费4年开发
桌面RTX 4070玩游戏 显示器选2K还是4K?来看对比实测 全球热推荐
远离元宇宙后 扎克伯格财富暴涨3000亿 全球最多!|全球新视野
小米两大技术接入联发科平台!卢伟冰:天玑芯片影像蜕变
37岁姐姐跟98年男友520领证 网友:羡慕了 天天讯息
全球观察:直通车是什么快递_直通车是什么
虎书 第一章 图形流水线_环球焦点
10.998万元 春风1250TR-G摩托价格公布:用上75°V型双缸发动机
焦点速看:丰田再批电动车毫无意义:烧煤发电不环保 氢燃料才合理
【环球时快讯】瓦伦丁·迪奥曼德_关于瓦伦丁·迪奥曼德介绍
C++ Today01
Kafka未触发消费异常排查实录-全球实时
基于python实现-根据Excel表格指定的UniqueKey的顺序-到另一个参考表格中查找-补全与自己相关的数据
Revit二次开发实战
天天观察:年轻人发完红包看电影 520单日总票房超1.5亿:速激10位列第一
网吧用“蛤蟆”、“天鹅”区分男女厕所 网友点赞直呼有才:官方回应
樱桃自由了!水果贵族樱桃一斤直降30元 六七月份还会继续降
B站CEO陈睿:年轻人爱学习 超8成985、211学生是B站用户 天天速递
三星独占结束!国产手机将用上“鸡血版”二代骁龙8-当前视点
浏阳:残疾人以“武”会友,散发的是自信和快乐-全球资讯
OPEN AI角色插件通道开放接入支持各种细分领域对话场角色景模型一键接入AI 智能 聚焦
记录--Vue中如何导出excel表格
马斯克回避的赛道蔚来进军了:可控核聚变20年内商用_环球热点
首发联发科8200-Ultra!小米Civi 3跑分出炉|全球即时
新疆乌鲁木齐达坂城姑娘特色农产品展示中心预计7月投用|当前速讯
网约车拒违停 2乘客赖车上超24小时还原地撒尿 官方通报:行拘-新要闻
告别MMO手游氪金!《逆水寒》手游公测预告片发布:6月30日见
黄百鸣谈为何拍《叶问5》:前几部成绩好 甄子丹也想拍 天天报道
李荣浩沉迷《王国之泪》 吐槽吉波得女王太难:不可能有人打的过 世界速递
环球快看:Revit二次开发 知识点总结(表格)
环球今亮点!荣耀:再见了,高价低配!骁龙8+芯片+IMX800仅2599元
老车主退10万差价业界良心!极狐阿尔法S HI先行版上市:32.98万_世界观速讯
微信开放520元红包:特别的爱也可以是最多1000元 世界简讯
当前要闻:synopsys dw_axi_dmac 使用集成经验
直播源码技术控制直播稳定之消息篇
全新一代华为海思芯片V811首发!七大绝技 支持8K30Hz解码-每日简讯
明道回应与蒋雯丽拍戏争议 姐弟恋新剧《转角之恋》口碑翻车:网友直呼辣眼|当前关注
焦点消息!安徽大别山区和沿江江南部分地区将有大雨到暴雨
当前简讯:4K分辨率搭配全玻璃镜头!当贝F6投影仪发布:新一代海思V811芯片 支持8K解码
网易1.5折甩卖暴雪“分手遗产” 有人狂抢4箱转卖:暴雪国服仍没人接_当前热点
拉脱维亚第20届“汉语桥”中文比赛落下帷幕
组件化编程
速看:使用ln命令在Linux系统中创建连接文件
当前观点:400元档性能天花板!当贝盒子H3S发布:8K HDR、3+32GB存储
华为等怎么看?报告称5G网放缓 运营商赔本减少投入:4G更成熟 最新快讯