实用指南:【每日一面】React Hooks闭包陷阱

news/2025/10/13 23:00:29/文章来源:https://www.cnblogs.com/wzzkaifa/p/19139615

基础问答

问题:谈一谈你对 React Hook的闭包陷阱的理解。

产生问题的原因:JavaScript 闭包特性 + Hooks 渲染机制

闭包的本质:函数能够访问其定义时所在的词法作用域,即使函数在作用域外执行,也可以记住定义时的词法作用域的内容,后续执行时,使用这些信息。

function callback(index) {
let idx = index;
let op;
return (type) => {
op = type;
console.log(op);
switch (type) {
case 'add':
idx++;
break;
case 'sub':
idx--;
break;
}
return idx;
}
}
const fn = callback(8);
console.log(fn('add')); // 9
console.log(fn('sub')); // 8

这里的 idx 正常会在 callback 函数执行结束后释放,但是由于我们返回的是一个函数,函数中依赖这个 idx 变量,所以未能释放,此时这个变量被这个匿名函数持有,而在 fn 变量存续期间,idx 和 op 都是不会释放的,这也就形成了一个闭包。

不过经典闭包还是 for 循环

Hooks 渲染逻辑:React 组件每次渲染都是独立的快照,可以理解为,每次重新执行相关钩子的时候,组件都会重新生成一个新的作用域。

闭包陷阱:根据上面两点,React Hooks 的闭包陷阱产生过程应当是这样的,React 在渲染开始前创建了新的状态包(作用域),而我们写代码的时候无意中创建了一个闭包,持有了 React 的当前状态,再下次渲染开始时,React 重新创建了状态包,但是我们在一开始创建的闭包持有的依旧是前一次 React 创建的状态,是旧的,这就是产生闭包陷阱的根源。这里我们以一个具体例子来看:

import { useEffect, useState } from "react"
const App = () => {
const [count, setCount] = useState(1);
useEffect(()=> {
const timer = setInterval(() => console.log(count), 1000);
return () => clearInterval(timer)
}, []);
const addOne = () => {
setCount(pre => pre+1);
}
return (
<div className="main" ><p>Hello: {count}</p><button onClick={addOne}>+1</button></div>)}export default App

这里在组件首次渲染的时候,useEffect 帮我们设置了一个定时器,定时器执行的函数持有了外部作用域的 count 变量,产生了一个闭包。

再之后,我们在页面上点击按钮时,触发了 setCount(pre => pre+1) 状态更新,但是由于没有配置 useEffect 的更新依赖,所以定时器还是持有旧的状态包。此时打印的还是 1,没有更新。

闭包陷阱破解方式

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

扩展知识

React 官方要求我们不能将 hooks 用 if 条件判断包裹,其原因是 React 的 Fiber 架构中收集 Hooks 信息的时候是按顺序收集的,并以链表的形式进行存储的。如下示例:

function App() {
const [count, setCount] = useState(0);
const [isFirst, setIsFirst] = useState(false);
useEffect(() => {
console.log('hello init');
}, []);
useEffect(() => {
console.log('count change: ', count);
}, [count]);
const a = 1;
}

示例中存在 4 个 hooks,所以 React 收集完成后形成的链表应当是这样的:

链表图

React 为链表节点设计了如下数据结构:

type Hook = {
memoizedState: any,
/** 省略这里不需要的内容 */
next: Hook | null,
};

其中 next 就是链表节点用于指向下一个节点的指针,memoizedState 则是上一次更新后的相关 state。组件更新的时候,hooks 会严格按照这个顺序进行执行,按顺序拿到对应的 Hook 对象,所以如果我们用 if else 包裹了其中一个 hook,就会出现链表执行过程中,Hooks 对象取值错误的情况。

同样的,React 官方告诉我们,如果想在更新的时候拿到当前 state 的值,建议使用回调函数的写法,即:setCount(pre => pre + 1) 这种写法,这个原因,通过 Hook 的数据结构也大致可以判断,因为 memoizedState 存储了前一次更新的数据,使用回调时,这个 memoizedState 就可以作为参数提供给我们,并且保证总是正确的。

面试追问

  1. 能手写一个闭包吗?

参考前文代码。

  1. 使用 useRef 存储值,会有什么问题?

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

  1. 请谈谈 hooks 在 React 中的更新逻辑?

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

  1. 那 hooks 中,useState 的更新是同步还是异步?

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

  1. useEffect 依赖数组传空数组和不传依赖,二者有什么区别?

空数组:effect 仅在组件首次渲染时执行一次,后续不会再执行,相当于组件挂载阶段。

不传依赖:effect 会在组件首次渲染时、每次重新渲染后都执行。这种形式隐含存在渲染循环的风险,即 effect 中存在修改 state 的操作,那么按照不传依赖时执行的规则,就会陷入渲染 -> 更新 -> 触发重渲染 -> 更新 -> 触发重渲染……这样的循环。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/936506.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java 面试 - krt

1、ArrayList和LinkedList的区别 2、@Autowired和@Resource的主要区别1、ArrayList和LinkedList的区别 ArrayList:底层数据结构:ArrayList基于数组实现,元素在内存中连续存储,支持随机访问(时间复杂度为o(1));…

软工大三开学总结

在本学期我的目标是主要是在完成本学期的课程目标拿到奖学金的同时跟进考研。 在考虑过很多之后我个人还是觉得考研是我的选择,因为我从心底里觉得,我好不容易 上到这儿了一说是吧,怎么能就止步于此呢,再向上走走呢…

SpringBoot-day2(基于SpringBoot实现SSMP整合) - a

SpringBoot JC-3.基于SpringBoot实现SSMP整合 ​ 重头戏来了,SpringBoot之所以好用,就是它能方便快捷的整合其他技术,这一部分咱们就来聊聊一些技术的整合方式,通过这一章的学习,大家能够感受到SpringBoot到底有多…

给一个字符串数组,输出不同的部分

豆包给出代码/// <summary> /// 字符串差异提取工具类(仅使用原字符串字符) /// </summary> public static class StringDiffExtractor {/// <summary>/// 从字符串列表中提取差异部分,差异部分仅…

Java按顺序提取Word内容(文本+数学公式) - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

连接 USB 设备

转载自:https://learn.microsoft.com/zh-cn/windows/wsl/connect-usb本指南将演练使用 USB/IP 开源项目 usbipd-win 将 USB 设备连接到 WSL 2 上运行的 Linux 分发版所需的步骤。 在 Windows 计算机上配置 USB/IP 项目…

实用指南:嵌入式学习笔记3.基于寄存器方式控制GPIO

实用指南:嵌入式学习笔记3.基于寄存器方式控制GPIO2025-10-13 22:50 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; disp…

# 20232429 2025-2026-1 《网络与系统攻防技术》实验一实验报告

1.实验内容手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。 注入一个自己制作的shellcode并运行这段shellcode。 …

muduo网络库事件驱动模型的实现与架构 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

SpringBoot-day1(快速上手SpringBoot,SpringBoot简介,SpringBoot基础配置,属性配置,yaml文件) - a

SpringBoot 文档更新日志版本 更新日期 操作 描述v1.0 2021/11/14 A 基础篇前言 ​ 很荣幸有机会能以这样的形式和互联网上的各位小伙伴一起学习交流技术课程,这次给大家带来的是Spring家族中比较重要的一门技术课程…

Chroma私有化:本地部署完整方案

嵌入向量(vector embedding)是表示任何类型数据的 A.I 原生方式,使它们非常适合与各种 A.I 驱动的工具和算法一起使用。 它们可以表示文本、图像,很快还可以表示音频和视频。 有许多创建嵌入的选项,无论是在本地…

嵌入式-C++面经2

一、问题总览cpp重载和重写的区别 cpp虚函数表 指针和引用的区别 linux的常用开发指令 linux编译运行程序的指令 关键字inline 什么场景使用内联 如何避免内存泄露 map和unordered_map 引用外部头文件双引号和尖括号的…

elk time

elk time- "/etc/localtime:/etc/localtime:ro"

PHP转Go系列 | 如何将 PHP 项目快速迁移到 Go 上?

大家好,我是码农先森。 最近在闲逛 v2ex 社区时,看到有个讨论 PHP 项目能否直接迁移到 Go 语言上的话题。我大概简述一下提问v友的原话,他们因为项目性能的问题在 2020 年时,从 Laravel 框架迁移到了 Hyperf 框架,…

详细介绍:【OpenHarmony】用户文件服务模块架构

详细介绍:【OpenHarmony】用户文件服务模块架构pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", …

详细介绍:全新 CloudPilot AI:嵌入 Kubernetes 的 SRE Agent,降本与韧性双提升!

详细介绍:全新 CloudPilot AI:嵌入 Kubernetes 的 SRE Agent,降本与韧性双提升!pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; fo…

“环境变量”是什么, 为什么要配置环境变量 --初学者

你好!本篇旨在用精炼、通俗的语言,帮助初学者快速理解“环境变量”的核心概念。你好!本篇旨在用精炼、通俗的语言,帮助初学者快速理解“环境变量”的核心概念。1. 环境变量是什么? 环境变量(Environment Variabl…

AI元人文:对大模型的召唤——未来哪吒

AI元人文:对大模型的召唤——未来哪吒 ——从价值仓库到文明对话的升维之路 我们站在一个历史的岔路口。眼前的大模型,是沉睡的文明巨兽,其千亿参数中封存着人类千年的智慧、冲突与渴望。它拥有价值的全集,却困于表…