找回密码
 立即注册
首页 业界区 业界 【每日一面】React Hooks闭包陷阱

【每日一面】React Hooks闭包陷阱

迭婵椟 5 天前
基础问答

问题:谈一谈你对 React Hook的闭包陷阱的理解。
产生问题的原因:JavaScript 闭包特性 + Hooks 渲染机制
闭包的本质:函数能够访问其定义时所在的词法作用域,即使函数在作用域外执行,也可以记住定义时的词法作用域的内容,后续执行时,使用这些信息。
  1. function callback(index) {
  2.   let idx = index;
  3.   let op;
  4.   return (type) => {
  5.     op = type;
  6.     console.log(op);
  7.     switch (type) {
  8.       case 'add':
  9.         idx++;
  10.         break;
  11.       case 'sub':
  12.         idx--;
  13.         break;
  14.     }
  15.     return idx;
  16.   }
  17. }
  18. const fn = callback(8);
  19. console.log(fn('add')); // 9
  20. console.log(fn('sub')); // 8
复制代码
这里的 idx 正常会在 callback 函数执行结束后释放,但是由于我们返回的是一个函数,函数中依赖这个 idx 变量,所以未能释放,此时这个变量被这个匿名函数持有,而在 fn 变量存续期间,idx 和 op 都是不会释放的,这也就形成了一个闭包。
不过经典闭包还是 for 循环
Hooks 渲染逻辑:React 组件每次渲染都是独立的快照,可以理解为,每次重新执行相关钩子的时候,组件都会重新生成一个新的作用域。
闭包陷阱:根据上面两点,React Hooks 的闭包陷阱产生过程应当是这样的,React 在渲染开始前创建了新的状态包(作用域),而我们写代码的时候无意中创建了一个闭包,持有了 React 的当前状态,再下次渲染开始时,React 重新创建了状态包,但是我们在一开始创建的闭包持有的依旧是前一次 React 创建的状态,是旧的,这就是产生闭包陷阱的根源。这里我们以一个具体例子来看:
  1. import { useEffect, useState } from "react"
  2. const App = () => {
  3.   const [count, setCount] = useState(1);
  4.   useEffect(()=> {
  5.     const timer = setInterval(() => console.log(count), 1000);
  6.     return () => clearInterval(timer)
  7.   }, []);
  8.   const addOne = () => {
  9.     setCount(pre => pre+1);
  10.   }
  11.   return (
  12.      
  13.       <p>Hello: {count}</p>
  14.       <button onClick={addOne}>+1</button>
  15.    
  16.   )
  17. }
  18. export default App
复制代码
这里在组件首次渲染的时候,useEffect 帮我们设置了一个定时器,定时器执行的函数持有了外部作用域的 count 变量,产生了一个闭包。
再之后,我们在页面上点击按钮时,触发了 setCount(pre => pre+1) 状态更新,但是由于没有配置 useEffect 的更新依赖,所以定时器还是持有旧的状态包。此时打印的还是 1,没有更新。
闭包陷阱破解方式

  • 使用 useRef:useRef 在初始化后,是一个形如 { current: xxx } 的不可变对象,不可变可以理解为,这个对象的地址不会发生变化,所以在浅层次的比较(===)中,更新后的前后对象是一个。所以取值的时候,总是能拿到最新的值。
  • 添加 Hooks 依赖:在 useEffect 钩子的依赖列表中增加 count,当 count 发生变化的时候,会重新执行 useEffect ,内部的 timer 会重新生成,拿到最新的作用域的值。
  • 修改 state 为一个对象:类似于 useRef,我们在更新 state 的时候,可以直接把内容写入该对象中,避免直接替换 state 对象。
扩展知识

React 官方要求我们不能将 hooks 用 if 条件判断包裹,其原因是 React 的 Fiber 架构中收集 Hooks 信息的时候是按顺序收集的,并以链表的形式进行存储的。如下示例:
  1. function App() {
  2.   const [count, setCount] = useState(0);
  3.   const [isFirst, setIsFirst] = useState(false);
  4.   useEffect(() => {
  5.     console.log('hello init');
  6.   }, []);
  7.   useEffect(() => {
  8.     console.log('count change: ', count);
  9.   }, [count]);
  10.   const a = 1;
  11. }
复制代码
示例中存在 4 个 hooks,所以 React 收集完成后形成的链表应当是这样的:
1.png

React 为链表节点设计了如下数据结构:
  1. type Hook = {
  2.   memoizedState: any,
  3.   /** 省略这里不需要的内容 */
  4.   next: Hook | null,
  5. };
复制代码
其中 next 就是链表节点用于指向下一个节点的指针,memoizedState 则是上一次更新后的相关 state。组件更新的时候,hooks 会严格按照这个顺序进行执行,按顺序拿到对应的 Hook 对象,所以如果我们用 if else 包裹了其中一个 hook,就会出现链表执行过程中,Hooks 对象取值错误的情况。
同样的,React 官方告诉我们,如果想在更新的时候拿到当前 state 的值,建议使用回调函数的写法,即:setCount(pre => pre + 1) 这种写法,这个原因,通过 Hook 的数据结构也大致可以判断,因为 memoizedState 存储了前一次更新的数据,使用回调时,这个 memoizedState 就可以作为参数提供给我们,并且保证总是正确的。
面试追问


  • 能手写一个闭包吗?
参考前文代码。

  • 使用 useRef 存储值,会有什么问题?
useRef 在初始化后,是形如 { current: xxx } 的对象,这个对象地址不会变化,所以我们监听 ref 是不起作用的,同时,和 useState 不同,useRef 内容的变更不会触发组件重新渲染。

  • 请谈谈 hooks 在 React 中的更新逻辑?
React 是以链表形式来组织管理 hooks 的,在收集过程中按照顺序组装成链表,然后每次触发状态更新时,会从链表头开始依次判断执行更新。

  • 那 hooks 中,useState 的更新是同步还是异步?
可以理解为异步的,展开来说,则是: state 更新函数(如触发 setCount)是同步触发的,React 执行更新(即 count 被更新)是异步的。这种设计主要是出于性能考虑,避免重复渲染,减少重绘重排。

  • useEffect 依赖数组传空数组和不传依赖,二者有什么区别?
空数组:effect 仅在组件首次渲染时执行一次,后续不会再执行,相当于组件挂载阶段。
不传依赖:effect 会在组件首次渲染时、每次重新渲染后都执行。这种形式隐含存在渲染循环的风险,即 effect 中存在修改 state 的操作,那么按照不传依赖时执行的规则,就会陷入渲染 -> 更新 -> 触发重渲染 -> 更新 -> 触发重渲染……这样的循环。
友情链接:webfem.com
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册