浅谈react useEffect闭包的坑


Posted in Javascript onJune 08, 2021

问题代码

看一段因为useEffect导致的闭包问题代码

const btn = useRef();
const [v, setV] = useState('');

useEffect(() => {
    let clickHandle = () => {
        console.log('v:', v);
    }
    btn.current.addEventListener('click', clickHandle)
    
    return () => {
        btn.removeEventListener('click', clickHandle)
    }
}, []);
    
const inputHandle = e => {
    setV(e.target.value)
}

return (
        <>
            <input value={v} onChange={inputHandle} />
            <button ref={btn} >测试</button>
        </>
    )

useEffect的依赖项数组为空,所以在页面渲染完成之后,内部代码只会执行一次,页面销毁再执行一次。此时在输入框中输入任意字符,再点击测试按钮,得到的输出为空,之后无论如何输入任何字符,再点击测试按钮时,输出的结果仍为空。

为什么会这样呢?其实就是闭包所造成的。

产生原因

函数的作用域在函数定义的时候就决定了

给btn注册点击事件时,作用域如下:

浅谈react useEffect闭包的坑

能访问到的自由变量v此时还是空值。当点击事件触发时,执行点击回调函数,此时先创建执行上下文,会拷贝作用域链到执行上下文中。

  • 如果未在输入框内输入字符,此时点击拿到的v还是原来那个v
  • 如果在输入框内输入了字符,此时调用了setV修改了state,页面触发render,组件内部代码会重新执行一遍,重新声明了一个v,v就不再是原来那个v,这里点击事件里作用域中的v还是旧的v,这是两个不同的v

产生场景

  • 事件绑定。比如示例代码中,在页面最初渲染完成后只绑定一次事件的情况,比如使用echarts,在useEffect中获取echarts的实例并绑定事件
  • 定时器。页面加载后注册一个定时器,定时器内的函数也会产生如此的闭包问题。

解决办法

针对这个闭包问题下面大致给出5种解决办法

1. 以赋值方式直接修改v,并将修改v的方法用useCallback包裹起来

将修改v的方法用useCallback包裹起来,被useCallback包裹的函数将被缓存,由于依赖项的数组为空,所以这里直接赋值的方式修改的v是旧的v,此种方法不推荐,因为setState才是官方推荐的修改state的方式,这里仍然使用setV只是为了触发rerender

// v 的声明 由 const 改为 var,方便直接修改
var [v, setV] = useState('');

const inputHandle = useCallback(e => {
    let { value } = e.target
    v = value
    setV(value)
}, [])

2. 给useEffect的依赖项加上v

这也许是大多数人首先想到的办法,既然v是旧的,那么每次v更新的时候,重新注册一次事件不就行了,但是这样的会导致每次v更新都得重新注册,理论应该只需要注册一次的事件变成了多次。

3. 避免v被重新声明

以let或var的方式声明某个变量代替v,直接修改这个变量,而不是要setState相关函数触发render,这样就不会被重新声明,点击的回调函数里就能拿到“最新”的值,但这个方法更不推荐,就此示例来说,input组件由于没有rerender而至始至终都是显示空值,不符合操作预期。

4. 使用useRef代替useState

const btn = useRef();
const vRef = useRef('');
const [v, setV] = useStat('');

useEffect(() => {
    let clickHandle = () => {
        console.log('v:', vRef.current);
    }
    btn.current.addEventListener('click', clickHandle)
    
    return () => {
        btn.removeEventListener('click', clickHandle)
    }
}, []);

const inputHandle = e => {
    let { value } = e.target
    vRef.current = value
    setV(value)
}

return (
        <>
            <input value={v} onChange={inputHandle} />
            <button ref={btn} >测试</button>
        </>
    )

useRef的方案之所以有效,是因为每次input的change修改的是vRef这个对象的current属性,而vRef始终是那个vRef,即使rerender,由于vRef是对象,所以变量存储在栈内存中的值是该对象在堆内存中的地址,只是一个引用,只修改对象的某个属性,该引用并不会改变。所以点击事件中的作用域链始终访问的都是同一个vRef

浅谈react useEffect闭包的坑

5. 将v换成对象类型

其实和使用useRef一样,只要是对象,仅修改某个属性也不会改变该state所指向的地址。

代码地址

这里看测试代码

到此这篇关于浅谈react useEffect闭包的坑的文章就介绍到这了,更多相关react useEffect闭包内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木! 

Javascript 相关文章推荐
Js+Dhtml:WEB程序员简易开发工具包(预先体验版)
Nov 07 Javascript
设置checkbox为只读(readOnly)的两种方式
Oct 11 Javascript
node.js中的fs.linkSync方法使用说明
Dec 15 Javascript
js实现拖拽效果(构造函数)
Dec 14 Javascript
javascript合并表格单元格实例代码
Jan 03 Javascript
JavaScript中闭包之浅析解读(必看篇)
Aug 25 Javascript
vue+Java后端进行调试时解决跨域问题的方式
Oct 19 Javascript
Angular17之Angular自定义指令详解
Jan 21 Javascript
解决Vue不能检测数组或对象变动的问题
Feb 24 Javascript
jQuery实现checkbox全选功能完整实例
Jul 12 jQuery
javascript-hashchange事件和历史状态管理实例分析
Apr 18 Javascript
antd form表单数据回显操作
Nov 02 Javascript
Vue中插槽slot的使用方法与应用场景详析
vue+elementui 实现新增和修改共用一个弹框的完整代码
解决vue $http的get和post请求跨域问题
vue Element-ui表格实现树形结构表格
Jun 07 #Vue.js
Vue中foreach数组与js中遍历数组的写法说明
Jun 05 #Vue.js
浅谈JS的原型和原型链
vue响应式原理与双向数据的深入解析
You might like
php zlib压缩和解压缩swf文件的代码
2008/12/30 PHP
php 根据url自动生成缩略图并处理高并发问题
2014/01/23 PHP
Zend Framework入门应用实例详解
2016/12/11 PHP
PHP常用排序算法实例小结【基本排序,冒泡排序,快速排序,插入排序】
2017/02/07 PHP
Prototype使用指南之string.js
2007/01/10 Javascript
javascript实现图片切换的幻灯片效果源代码
2012/12/12 Javascript
Jquery中val()表单取值赋值的实例代码
2013/08/15 Javascript
JavaScript的作用域和块级作用域概念理解
2014/09/21 Javascript
JavaScript极简入门教程(一):基础篇
2014/10/25 Javascript
Bootstrap学习笔记之css组件(3)
2016/06/07 Javascript
AngularJs自定义服务之实现签名和加密
2016/08/02 Javascript
Angular2 多级注入器详解及实例
2016/10/30 Javascript
jQuery与JavaScript节点创建方法的对比
2016/11/18 Javascript
微信小程序 自定义Toast实例代码
2017/06/12 Javascript
3种vue组件的书写形式
2017/11/29 Javascript
js实现中文实时时钟
2020/01/15 Javascript
Nuxt pages下不同的页面对应layout下的页面布局操作
2020/11/05 Javascript
解决vue项目本地启动时无法携带cookie的问题
2021/02/06 Vue.js
[00:36]DOTA2风云人物相约完美“圣”典 12月17日不见不散
2016/11/30 DOTA
python3实现读取chrome浏览器cookie
2016/06/19 Python
浅谈Python2获取中文文件名的编码问题
2018/01/09 Python
python selenium 执行完毕关闭chromedriver进程示例
2019/11/15 Python
pymysql模块的操作实例
2019/12/17 Python
python模块如何查看
2020/06/16 Python
GUESS盖尔斯法国官网:美国时尚品牌
2016/09/23 全球购物
美国Rue La La闪购网站:奢侈品、中高档品牌限时折扣
2016/10/19 全球购物
NULL是什么,它是怎么定义的
2015/05/09 面试题
文明风采获奖感言
2014/02/18 职场文书
公司试用期员工自我评价
2014/09/17 职场文书
党员先进性教育整改措施
2014/09/18 职场文书
中学生的1000字检讨书
2014/10/11 职场文书
单位委托书
2014/10/15 职场文书
2015年高校教师个人工作总结
2015/05/25 职场文书
家长会后的感想
2015/08/11 职场文书
java开发双人五子棋游戏
2022/05/06 Java/Android
Vue深入理解插槽slot的使用
2022/08/05 Vue.js