浅谈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 相关文章推荐
通过event对象的fromElement属性解决热区设置主实体的一个bug
Dec 22 Javascript
jquery js 获取时间差、时间格式具体代码
Jun 05 Javascript
使用JS CSS去除IE链接虚线框的三种方法
Nov 14 Javascript
原生JavaScript生成GUID的实现示例
Sep 05 Javascript
JavaScript实现的类字典插入或更新方法实例
Jul 10 Javascript
JavaScript脚本库编写的方法
Dec 09 Javascript
JavaScript中数组添加值和访问值常见问题
Feb 06 Javascript
jQuery中 $ 符号的冲突问题及解决方案
Nov 04 Javascript
easyui-combobox 实现简单的自动补全功能示例
Nov 08 Javascript
jstree单选功能的实现方法
Jun 07 Javascript
限时抢购-倒计时的完整实例(分享)
Sep 17 Javascript
小程序组件之仿微信通讯录的实现代码
Sep 12 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
PHPnow安装服务[apache_pn]失败的问题的解决方法
2010/09/10 PHP
phpmyadmin3 安装配置图解教程
2012/03/29 PHP
Linux服务器下PHPMailer发送邮件失败的问题解决
2017/03/04 PHP
PHP基于方差和标准差计算学生成绩的稳定性示例
2017/07/04 PHP
Sample script that displays all of the users in a given SQL Server DB
2007/06/16 Javascript
javascript中的变量是传值还是传址的?
2010/04/19 Javascript
JQuery拖拽元素改变大小尺寸实现代码
2012/12/10 Javascript
javascipt匹配单行和多行注释的正则表达式
2013/11/20 Javascript
node.js中的fs.chownSync方法使用说明
2014/12/16 Javascript
jQuery实现弹出窗口弹出div层的实例代码
2017/01/09 Javascript
原生js实现简单的Ripple按钮实例代码
2017/03/24 Javascript
JS 中document.write()的用法和清空的原因浅析
2017/12/04 Javascript
Vue2.0 slot分发内容与props验证的方法
2017/12/12 Javascript
json对象及数组键值的深度大小写转换问题详解
2018/03/30 Javascript
js指定日期增加指定月份的实现方法
2018/12/19 Javascript
Vue 组件修改根实例的数据的方法
2019/04/02 Javascript
微信小程序使用Vant Weapp组件库的方法步骤
2019/08/01 Javascript
基于vue-cli3创建libs库的实现方法
2019/12/04 Javascript
[52:03]Secret vs VG 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/20 DOTA
解决python写的windows服务不能启动的问题
2014/04/15 Python
Python中用于转换字母为小写的lower()方法使用简介
2015/05/19 Python
Python实现求两个csv文件交集的方法
2017/09/06 Python
python ftp 按目录结构上传下载的实现代码
2018/09/12 Python
PYTHON EVAL的用法及注意事项解析
2019/09/06 Python
在pycharm中为项目导入anacodna环境的操作方法
2020/02/12 Python
Python 之 Json序列化嵌套类方式
2020/02/27 Python
python代码实现TSNE降维数据可视化教程
2020/02/28 Python
使用HTML5的File实现base64和图片的互转
2013/08/01 HTML / CSS
英国第一摩托车和摩托车越野配件商店:GhostBikes
2019/03/10 全球购物
运动会跳远广播稿
2014/02/04 职场文书
公司成本主管岗位责任制
2014/02/21 职场文书
大学生社会实践自我鉴定
2014/03/24 职场文书
公司请假条范文
2014/04/11 职场文书
奥巴马的演讲稿
2014/05/15 职场文书
2014财务人员自我评价范文
2014/09/21 职场文书
学校运动会广播稿范文
2014/10/02 职场文书