当前位置: 首页 > news >正文

【React避坑指南】useEffect 依赖引用类型

前言

如果你是一个入行不久的前端开发,面试中多半会遇到一个问题:

你认为使用React要注意些什么?

这个问题意在考察你对React的使用深度,因为沉浸式地写过一个项目就会发现,不同于一些替你做决定的框架,“潜规则”丰富的React远比看上去要难相处。
React中主要有两类坑点,一种是让你措手不及,结果对不上预期,严重影响开发进度,另一种更为头痛,表面风平浪静,水下暗流涌动。
官方文档的触角只伸到Demo级别,并不涉及花样百出的最差实践,所以下一批开发者又会掉入相同的陷阱。隐藏的坑点需要开发者亲自下地扫雷,经验主义发挥了重要作用,尤其是在Hooks使用中。
为了避免更多的心智负担,这个系列的文章会介绍一些React使用的常见陷阱,带你追溯原因和探索解决方案,帮助新手迅速跳过坑点。

问题提出

const Issue = function () {const [count, setCount] = useState(0);const [person, setPerson] = useState({ name: 'Alice', age: 15 });const [array, setArray] = useState([1, 2, 3]);useEffect(() => {console.log('Component re-rendered by count');}, [count]);useEffect(() => {console.log('Component re-rendered by person');}, [person]);useEffect(() => {console.log('Component re-rendered by array');}, [array]);return (<div><p>You clicked {count} times</p><button onClick={() => setCount(1)}>Update Count</button><button onClick={() => setPerson({ name: 'Bob', age: 30 })}>Update Person</button><button onClick={() => setArray([1, 2, 3, 4])}>Update Array</button></div>);
};

在这个案例中,初始化了三个状态,和对应的三个副作用函数useEffect,理想状态是状态的值更新时才触发useEffect。
多次点击Update Count更新State,因为更新后的值还是1,所以第一个useEffect执行第一次后不会重复执行,这符合预期。但是重复点击Update Person和Update Array时,却不是这样,尽管值相同,但useEffect每一次都会触发。当useEffect中的副作用计算量较大时,必然会引起性能问题。

原因追溯

为了追溯这个原因,可以首先熟悉一下useEffect的源码:

function useEffect(create, deps) {const fiber = get();const { alternate } = fiber;if (alternate !== null) {const oldProps = alternate.memoizedProps;const [oldDeps, hasSameDeps] = areHookInputsEqual(deps, alternate.memoizedDeps);if (hasSameDeps) {pushEffect(fiber, oldProps, deps);return;}}const newEffect = create();pushEffect(fiber, newEffect, deps);
}function areHookInputsEqual(nextDeps, prevDeps) {if (prevDeps === null) {return false;}for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {if (Object.is(nextDeps[i], prevDeps[i])) {continue;}return false;}return true;
}

在上面的代码中,我们着重关注areHookInputsEqual的实现,这个函数对比了前后两次传入的依赖项,决定了后续副作用函数create()是否会执行。可以明显看到,useEffect对于依赖项执行的是浅比较,即Object.is (arg1, arg2),这可能是出于性能考虑。对于原始类型这没有问题,但对于引用类型(数组、对象、函数等),这意味着即使内部的值保持不变,引用本身也会发生变化,导致 useEffect执行副作用。

方案探索

1.饮鸩止渴

缝缝补补只是为了等一个人替你推倒重盖

最直接的思路是把useEffect的依赖项从引用类型换成基本类型:

  useEffect(() => {console.log('Component re-rendered by person');}, [JSON.stringify(person)]);useEffect(() => {console.log('Component re-rendered by array');}, [JSON.stringify(array)]);

表面上可行,实际后患无穷(具体参考JSON.stringify为什么不能用来深拷贝),为了避坑而挖另外的坑,显然不是我们期待的解决方案。
对比之下,这样的写法可以容忍,但是person对象如果增加了其他属性,你要确保自己还记得更新依赖,否则依然是掩盖问题。

useEffect(() => {console.log('Component re-rendered by person');
}, [person.name, person.age]);

2.前置拦截

第二种思路:

在你决定要出手之前,我已经帮你决定了 —— 格林公式引申公理

我们可以把问题尽可能前置,手动加一层深对比,如何发现引用值没有变化,就不执行状态更新的逻辑,也就不会触发useEffect重复执行。

<button onClick={() => {const newPerson = { name: 'Bob', age: 18 };if (!isEqual(newPerson, person)) {setPerson(newPerson)}}}
>Update person</button>

但这样显然不太优雅,且每一次写setState时心智负担太重,对比逻辑可不可以封装起来。

3.他山之石

实际上自定义的Hooks就是为了解决方法级别的逻辑复用,这里我们利用useRef绑定的值可以跨渲染周期的特点,实现一个自定义的useCompare。

const useCompare = (value, compare) => {const ref = useRef(null);if (!compare(value, ref.current)) {ref.current = value;}return ref.current;
}

经过ref记录的上一次结果,我们同时拥有了前后两次更新的状态,如果发现值不同,再让ref绑定新的引用类型地址。

import { isEqual } from 'lodash';const comparePerson = useCompare(person, isEqual);useEffect(() => {console.log('Component re-rendered by comparePerson');
}, [comparePerson]);// 重复执行
useEffect(() => {console.log('Component re-rendered by person');
}, [person]);

需要注意的是,这里使用了lodash的isEqual函数实现深对比,看似省心实际是一个成本极其不稳定的选择,如果对象过于庞大,可能得不偿失,可以传入简化的compare函数,有取舍的比较常变的key值。
而且每次又到单独调用useCompare生成新的对象,这里的逻辑也值得被封装。

4.回归本质

停止曲线救国,直面问题本身。

说了这么多,实际还是useEffect中对比逻辑问题,本着支持拓展但不支持修改的原则,我们需要支持一个新的useEffect支持深度对比。我们将useRef实现的记忆引用传入useEffect的对比逻辑中:

import { useEffect, useRef } from 'react';
import isEqual from 'lodash.isequal';const useDeepCompareEffect = (callback, dependencies, compare) => {// 默认的对比函数采用lodash.isEqual, 支持自定义if (!compare) compare = isEqual;const memoizedDependencies = useRef([]);if (!compare (memoizedDependencies.current, dependencies)) {memoizedDependencies.current = dependencies;}useEffect(callback, memoizedDependencies.current);
};export default useDeepCompareEffect;function App({ data }) {useDeepCompareEffect(() => {// 这里的代码只有在 data 发生深层级的改变时才会执行console.log('data 发生了改变', data);}, [data]);return <div>Hello World</div>;
}

考虑到前文提到的复杂对象的深对比隐患,我依然结和个人意志,在useDeepCompareEffect中加了一个可选参数compare函数,把isEqual作为一种默认模式。于是,我们终于有了一劳永逸的方法。

总结

实际上,react-use和a-hooks等第三方库都已经实现了useDeepCompareEffect,也可以发现自定义hooks解决问题将会是目前体系下一种复用性极高的实践。通过这些方法的推导,也可以看出我们获取方案的思路,希望对新手的成长有所帮助。

相关文章:

【React避坑指南】useEffect 依赖引用类型

前言 如果你是一个入行不久的前端开发&#xff0c;面试中多半会遇到一个问题&#xff1a; 你认为使用React要注意些什么&#xff1f; 这个问题意在考察你对React的使用深度&#xff0c;因为沉浸式地写过一个项目就会发现&#xff0c;不同于一些替你做决定的框架&#xff0c;“…...

Android binder通信实现进程间通信

一.binder通信原理Binder 是 Android 系统中用于跨进程通信的一种机制&#xff0c;它允许一个进程中的组件与另一个进程中的组件进行通信&#xff0c;从而实现进程间通信 (IPC)。Binder 机制是基于 Linux 内核提供的进程间通信机制 (IPC) 实现的。在 Binder 机制中&#xff0c;…...

2023年BeijngCrypt勒索病毒家族最新变种之.halo勒索病毒

目录 前言&#xff1a;简介 一、什么是.halo勒索病毒&#xff1f; 二、.halo勒索病毒是如何传播感染的&#xff1f; 三、感染.halo后缀勒索病毒建议立即做以下几件事情 四、中了.halo后缀的勒索病毒文件怎么恢复&#xff1f; 五、加密数据恢复情况 六、系统安全防护措施建…...

【LeetCode】BM1 反转链表、NC21 链表内指定区间反转

作者&#xff1a;小卢 专栏&#xff1a;《Leetcode》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 BM1 反转链表 描述&#xff1a; 给定一个单链表的头结点pHead(该头节点是有值的&#xff0c;…...

拼多多24届暑期实习真题

1. 题目描述&#xff1a; 多多开了一家自助餐厅&#xff0c;为了更好地管理库存&#xff0c;多多君每天需要对之前的课流量数据进行分析&#xff0c;并根据客流量的平均数和中位数来制定合理的备货策略。 2. 输入输出描述&#xff1a; 输入描述&#xff1a; 输入共两行&#x…...

JS高级知识总结

文章目录1. this指向问题2. 对象进阶2.1 对象的定义和使用2.2 对象访问器2.2.1 Getter2.2.2 Setter2.3 对象构造器2.4 对象原型2.4.1 prototype属性2.4.2 \_\_proto\_\_ 属性2.4.3 constructor属性2.4.4 原型链2.5 Object对象2.5.1 管理对象2.5.2 保护对象3. 函数进阶3.1 函数的…...

Jenkins+Docker+Maven+gitlab实现自动构建、远程发布

前言 一个项目完整的生命周期是从开发的coding阶段和coding阶段的质量测试&#xff0c;再到多次发布投入使用。目前大部分的测试阶段并不是从coding结束后开始的&#xff0c;而是和coding同步进行的。可能今天早上coding完成一个功能&#xff0c;下午就要投入测试。在这期间&a…...

centos7克隆虚拟机完成后的的一些配置介绍

系列文章目录 centos7配置静态网络常见问题归纳_张小鱼༒的博客-CSDN博客 文章目录 目录 系列文章目录 前言 一、配置Hadoop要下载的压缩包 1、下载对应版本的Hadoop压缩包 2、我们如何查看自己电脑的端口号 3、下载jdk对应的版本 二、虚拟机centos7克隆虚拟机完成后的一些基本…...

C语言/动态内存管理函数

C程序运行时&#xff0c;内存将被划分为三个区域&#xff0c;而动态开辟的内存区间位于堆区。 文章目录 前言 一、内存划分 二、malloc函数 三、calloc函数 四、realloc函数 五、free函数 总结 前言 在使用C语言编写程序时&#xff0c;使用动态内存是不可避免的&#x…...

华为OD机试题,用 Java 解【任务调度】问题

华为Od必看系列 华为OD机试 全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典使用说明 参加华为od机试,一定要注意不要…...

河南农业大学2023春蓝桥杯赛前训练第一场

A 滑板上楼梯 贪心 要求最少次数&#xff0c;尽量多跳三阶的&#xff0c;不能连续跳三阶&#xff0c;三阶后面一定要跟着一个一阶&#xff0c;相当于直接跳四阶 每次跳四阶都是两步&#xff08;3、1&#xff09;&#xff0c;如果 % 4 之后&#xff0c;正好剩下 3 &#xff0c…...

docker-dockerfile

1.常用保留字指令 FROM : 基础镜像MAINTAINER: 维护者姓名和邮箱RUN : Run ["可执行文件"&#xff0c;参数1]&#xff1b; Run [shell命令]EXPOSE: 暴露出的端口号WORKDIR: 登录后的位置USER: 执行用户,默认是rootENV: 构建过程的环境变量ADD: 将宿主机的文件拷贝到…...

【JavaEE】浅识进程

一、什么是进程1.1 操作系统学习进程之前首先要了解我们的操作系统&#xff08;OS&#xff09;&#xff0c;我们的操作系统实际上也是一款软件&#xff0c;属于系统软件的范畴&#xff0c;操作系统早期采用命令提示框与用户交互&#xff0c;我们启动某个软件&#xff0c;打开某…...

Java_Spring:1. Spring 概述

目录 1 spring 是什么 2 Spring 的发展历程 3 spring 的优势 4 spring 的体系结构 1 spring 是什么 Spring 是分层的 Java SE/EE 应用 full-stack 轻量级开源框架&#xff0c;以 IoC&#xff08;Inverse Of Control&#xff1a;反转控制&#xff09;和 AOP&#xff08;Aspec…...

使用Maven实现第一个Servlet程序

目录 前言&#xff1a; Maven 什么是Maven 创建Maven项目 Mevan目录介绍 Servlet程序 引入Servlet依赖 创建目录结构 编写代码 打包程序 部署程序 验证程序 idea集成Tomcat 下载Tomcat插件 配置Tomcat的路径 Smart Tomcat工作原理 小结&#xff1a; 前言&#…...

【MySQL】MySQL的优化(一)

目录 查看SQL执行频率 定位低效率执行SQL 定位低效率执行SQL-慢查询日志 定位低效率执行SQL-show processlist 查看SQL执行频率 MySQL 客户端连接成功后&#xff0c;通过 show [session|global] status 命令可以查看服务器状态信息。通 过查看状态信息可以查看对当…...

win kubernetes dashbord部署springboot服务

文章目录前言一、新建springboot工程二、制作镜像1.编写dockerfile2.使用阿里云镜像仓库3.使用dashbord部署服务总结前言 使用win版docker desktop安装的k8s&#xff0c;kubenetes dashbord。 一、新建springboot工程 就是简单一个接口。没什么说的 二、制作镜像 1.编写dock…...

Linux之进程终止

本节目录1.进程终止2.exit与_exit函数1.进程终止 进程终止时&#xff0c;操作系统做了什么&#xff1f; 释放进程中申请的相关内核数据结构和对应的数据和代码。本质就是释放系统资源。 进程终止的常见方式 a.代码跑完&#xff0c;结果正确 b.代码跑完&#xff0c;结果不正确…...

全网独家首发|极致版YOLOv7改进大提升(推荐)网络配置文件仅24层!更清晰更方便更快的改进YOLOv7网络模型

有不少小伙伴和我交流YOLO改进的时候&#xff0c;都说YOLOv7的网络配置文件长达104层&#xff0c;改起来很费力&#xff0c;数层数都要数很久&#xff0c;还很容易出错&#xff0c;而且基于YOLOv5代码架构&#xff0c;Debug起来也确实比较费时&#xff0c;所以博主对YOLOv7网络…...

C++入门 谁都能看懂的类和对象

类 C语言结构体中只能定义变量. 在C中&#xff0c;结构体内不仅可以定义变量&#xff0c;也可以定义函数。 //c语言 typedef struct ListNode {int val;struct ListNode* next; }LTN; //c struct ListNode {int val;//c中可以直接用这个&#xff0c;不用加structListNode* next…...

在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能

下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能&#xff0c;包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

AI Agent与Agentic AI:原理、应用、挑战与未来展望

文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例&#xff1a;使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例&#xff1a;使用OpenAI GPT-3进…...

鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院挂号小程序

一、开发准备 ​​环境搭建​​&#xff1a; 安装DevEco Studio 3.0或更高版本配置HarmonyOS SDK申请开发者账号 ​​项目创建​​&#xff1a; File > New > Create Project > Application (选择"Empty Ability") 二、核心功能实现 1. 医院科室展示 /…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

vue3+vite项目中使用.env文件环境变量方法

vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量&#xff0c;这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...

初学 pytest 记录

安装 pip install pytest用例可以是函数也可以是类中的方法 def test_func():print()class TestAdd: # def __init__(self): 在 pytest 中不可以使用__init__方法 # self.cc 12345 pytest.mark.api def test_str(self):res add(1, 2)assert res 12def test_int(self):r…...

【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习

禁止商业或二改转载&#xff0c;仅供自学使用&#xff0c;侵权必究&#xff0c;如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...

虚拟电厂发展三大趋势:市场化、技术主导、车网互联

市场化&#xff1a;从政策驱动到多元盈利 政策全面赋能 2025年4月&#xff0c;国家发改委、能源局发布《关于加快推进虚拟电厂发展的指导意见》&#xff0c;首次明确虚拟电厂为“独立市场主体”&#xff0c;提出硬性目标&#xff1a;2027年全国调节能力≥2000万千瓦&#xff0…...

沙箱虚拟化技术虚拟机容器之间的关系详解

问题 沙箱、虚拟化、容器三者分开一一介绍的话我知道他们各自都是什么东西&#xff0c;但是如果把三者放在一起&#xff0c;它们之间到底什么关系&#xff1f;又有什么联系呢&#xff1f;我不是很明白&#xff01;&#xff01;&#xff01; 就比如说&#xff1a; 沙箱&#…...

使用SSE解决获取状态不一致问题

使用SSE解决获取状态不一致问题 1. 问题描述2. SSE介绍2.1 SSE 的工作原理2.2 SSE 的事件格式规范2.3 SSE与其他技术对比2.4 SSE 的优缺点 3. 实战代码 1. 问题描述 目前做的一个功能是上传多个文件&#xff0c;这个上传文件是整体功能的一部分&#xff0c;文件在上传的过程中…...