“ 近期由于工作忙的原因,导致许久没有更新公众号了,现在抽空我们来了解一下React Hooks闭包相关的问题。”
学过js的同学,肯定对闭包一点都不陌生吧。
举个例子:
1 | // 前提:函数外部是无法读取函数内部变量 |
闭包概念:
简单理解为:能够读取其他函数内部变量的函数。由于在js语言中,只有函数内部的子函数才能去读取局部变量,我们还可以把闭包理解为:定义在一个函数内部的函数。本质:闭包就是将函数内部和函数外部连接起来的一座桥梁(你可以把它当成私有变量的get和set方法)。
闭包的作用:
可以读取函数内部的变量(上面例子中提到的);
让这些变量的值始终保持在内存中。
为什么始终保持在内存中?由于函数f1是f2的父函数,函数f2被赋值给res一个全局变量,就会导致函数f2始终在内存中,由于函数f2存在依赖函数f1,所有函数f1也始终在内容中,导致不会在调用结束后被垃圾回收机制回收。
闭包的应用场景:
数据私有化;
函数的封装与改造;
手动延长某些局部变量的寿命。
闭包的注意事项:
由于闭包会使得函数中的变量被保存在内存中,内存消耗很大,因此不能滥用闭包,否则会造成内存泄漏,出现性能问题(解决办法:在退出函数之前,将不使用的局部变量全部删除);
闭包会在父函数外部改变父函数内部变量的值,因此在使用闭包时不要随便的改变父函数内部变量的值。
首先先看一个例子:
1 | import React, { useState, useEffect } from 'react' |
在useEffect回调函数中我们会发现count打印的值始终为1,为什么count值没有跟着变呢?这就是React Hooks的闭包陷阱!
React Hooks原理:Hooks就是在fiber节点上存放了memoizedState链表,不同的hook在memoizedState链表不同的元素上存取值,代码如下:
1 | // Fiber与Hook的部分结构定义 |
我们还需要了解的就是:Hooks与Fiber是如何共同工作的,先看看下面两张在网络上找到的图片:
Hooks组成的链表结果图
在每个状态Hook节点中(如useState),会通过queue属性上的循环链表记住所有更新操作,并在update阶段依次执行循环链表中的所有更新操作,最终拿到最新的state返回。
副作用Hooks组成的链表结构
在每个副作用Hook节点中(如useEffect),创建effect挂载到Hook的memoizedState中,并添加到环形链表的末尾,该链表会保存到Fiber节点的updateQueue中,会在commit阶段执行。
大致介绍了一下React Hooks的原理之后,我们再回顾上面的代码,useEffect中的deps传递的是空数组,所以只会执行一次,然后我们又点击了1次button按钮试图去改变count的值变为2;那么这时问题就出来了,因为定时器中是要去拿到最新的count值,也就是说useEffect需要去依赖count的;deps是一个空数组,count的值是最开始初始化时的值为1,并且在定时器的回调函数中count值被引用了,所以就形成了闭包一直被保存(相当于它始终捕获的是第一个函数调用时产生的词法环境,因此捕获的count值始终为1)。看下面这幅图:
useEffect中deps为空数组解析图
虽然state变化了,但是执行的回调函数却一直引用着最开始的state。如何解决这个问题呢?答案其实很简单,就是给deps添加上count引用就可以了,然后就得到了下面这幅图:
useEffect中deps添加count引用解析图
闭包陷阱产生本质原因:useEffect、useCallback等hook里用到了某个state,但是没有添加到deps数组中,这样就导致了state变了却没有执行新传入的函数,依然引用的是之前的state。其实要解决闭包陷阱也很简单,那么就是正确的设置deps数组,这样每次用到的state变了就会执行新的函数,引入新的state。
如果不依赖deps数组,那我们应该如何解决这个棘手的问题呢?其实可以考虑使用useRef。因为useRef在memoizedState链表中放一个对象,current是用来保存值的,如果后面修改了ref中的值,那么取出来的就是最新的值。
特别感谢下面几篇文章的作者:
从react hooks“闭包陷阱”切入,浅谈react hooks