背景: 之前将React的基础知识以及状态管理相关的知识都过了一遍,查漏补缺的同时对React也有了一些新鲜的认知,接下来这个模块的名字很有意思:脱围机制,内容也比之前的部分难理解一些。但整体看下来,理解之后对React的使用上也会更上一层楼。就继续学习吧~

前期回顾:
重学React(一):描述UI
重学React(二):添加交互
重学React(三):状态管理
重学React(四):状态管理二

学习内容:

React官网教程:https://zh-hans.react.dev/learn/escape-hatches
其他辅助资料(看到再补充)
补充说明:这次学习更多的是以学习笔记的形式记录,看到哪记到哪

什么是脱围机制
在React中,除了React之外,我们还需要连接外部系统,比如需要连接服务器接口,获取服务器传来的数据,再比如操作DOM方法,比如focus,scroll等等。这些功能的前提需要“跳出”React自身的渲染逻辑,所以被称为脱围机制。接下来就开始学习如何脱围吧~

使用 ref 引用值

在实际编码中,偶尔会遇到希望组件能记住某些信息,这些信息的修改不触发页面重新渲染,比如记录setTimeout的id,这个id本身跟渲染毫无关系,只是用来标识当前的计时器以及在卸载组件时销毁它,如果不记录下来,就很难实现销毁,容易造成内存泄漏,此时就需要使用ref

给组件添加ref
import { useRef } from 'react';export const App () {
// useRef返回一个current对象
// {  current: 0 } // current的value是向 useRef 传入的值,任何类型都可以const ref = useRef(0);	
}

在这里插入图片描述
可以使用ref.current 属性访问该 ref 的当前值。ref 是一个普通的 JavaScript 对象,具有可以被读取和修改的 current 属性。
这个值是有意被设置为可变的,意味着既可以读取它也可以写入它。就像一个 React 追踪不到的、用来存储组件信息的秘密“口袋”。

示例:制作秒表
import { useState, useRef } from 'react';export default function Stopwatch() {
// 记录开始时间和当前时间,因为这两个时间需要计算并渲染出最后的结果,所以使用state,实现实时渲染const [startTime, setStartTime] = useState(null);const [now, setNow] = useState(null);// 用来记录当前计时器的id,便于重置时clearInterval,它在页面重新渲染时不需要改变,而是在进行操作时手动处理,所以使用ref进行记录const intervalRef = useRef(null);
// 每次点击开始时,将当前时间和记录时间重置function handleStart() {setStartTime(Date.now());setNow(Date.now());clearInterval(intervalRef.current);intervalRef.current = setInterval(() => {// 每隔十秒更新当前时间setNow(Date.now());}, 10);}function handleStop() {clearInterval(intervalRef.current);}let secondsPassed = 0;// 每次渲染用当前时间减去开始时间,就能得到过去了多少时间if (startTime != null && now != null) {secondsPassed = (now - startTime) / 1000;}return (<><h1>时间过去了: {secondsPassed.toFixed(3)}</h1><button onClick={handleStart}>开始</button><button onClick={handleStop}>停止</button></>);
}

ref 和 state 的不同之处

在这里插入图片描述

// React 内部,useRef的内部运行机制可以简单由useState实现
// 第一次渲染期间,useRef 返回 { current: initialValue }。 该对象由 React 存储,因此在下一次渲染期间将返回相同的对象。 
// 在这个示例中,state 设置函数没有被用到。它是不必要的,因为 useRef 总是需要返回相同的对象!
function useRef(initialValue) {const [ref, unused] = useState({ current: initialValue });return ref;
}
ref使用场景
  • 存储 timeout ID
  • 存储和操作 DOM 元素
  • 存储不需要被用来计算 JSX 的其他对象。
    总的来说,如果组件需要存储一些值,但不影响渲染逻辑,请选择 ref,这通常是不会影响组件外观的浏览器 API。
ref 的最佳实践

使用ref的原则

  • 将 ref 视为脱围机制。 在使用外部系统或浏览器 API 时,ref 很有用。但如果很大一部分应用程序逻辑和数据流都依赖于 ref,可能需要重新考虑方法是否有问题。
  • 不要在渲染过程中读取或写入 ref.current。 如果渲染过程中需要某些信息,请使用 state 代替。由于 React 不知道 ref.current 何时发生变化,即使在渲染时读取它也会使组件的行为难以预测。(唯一的例外是像 if (!ref.current) ref.current = new Thing() 这样的代码,它只在第一次渲染期间设置一次 ref。)
    ref本身就是一个普通的js对象,所以它的数据会实时更新,不会像state一样以快照的形式每隔一段时间才更新。所以只要ref的值不涉及渲染,React就不会关心你对 ref 或其内容做了什么。

使用Ref操作DOM

这是ref最常见的使用场景。在大部分情况下,React 会自动处理更新 DOM 以匹配渲染输出,所以不需要操作DOM。但在实现某些效果的情况下,比如控制DOM的滚动,让某个元素获得焦点等等,React没有内置方法,而是需要一个指向 DOM 节点的 ref 来实现。
接下来是具体的实现以及原理:

使文本输入框获得焦点
// 引入hook
import { useRef } from 'react';export default function Form() {
// 声明一个refconst inputRef = useRef(null);function handleClick() {// inputRef.current中保存的就是input节点,可以直接使用这个节点内置的API,这里使用的是focusinputRef.current.focus();}return (<>// 将 ref 作为 ref 属性值传递给想要获取的 DOM 节点的 JSX 标签<input ref={inputRef} /><button onClick={handleClick}>聚焦输入框</button></>);
}
如何使用 ref 回调管理 ref 列表

考虑一个场景:有n个列表,需要给每个列表都绑定一个ref,n的个数是未知的,所以我们不能预先将ref给一一声明了,因为 Hook 只能在组件的顶层被调用。所以不能在循环语句、条件语句或 map() 函数中调用 useRef 。解决这个问题有两种思路:

  1. 用一个 ref 引用其父元素,然后用 DOM 操作方法如 querySelectorAll 来寻找它的子节点。然而,这种方法很脆弱,如果 DOM 结构发生变化,可能会失效或报错
  2. ref 回调,也就是将函数传递给 ref 属性。当需要设置 ref 时,React 将传入 DOM 节点来调用 ref 回调,并在需要清除它时传入 null 。这可以维护自己的数组或 Map,并通过其索引或某种类型的 ID 访问任何 ref
    看个例子如何用第二个方法来解决问题:
    注意事项:启用严格模式后,ref 回调将在开发中运行两次
import { useRef, useState } from "react";export default function CatFriends() {const itemsRef = useRef(null);const [catList, setCatList] = useState(setupCatList);function scrollToCat(cat) {const map = getMap();const node = map.get(cat);node.scrollIntoView({behavior: "smooth",block: "nearest",inline: "center",});}function getMap() {if (!itemsRef.current) {// 首次运行时初始化 Map。itemsRef.current = new Map();}return itemsRef.current;}return (<><nav><button onClick={() => scrollToCat(catList[0])}>Neo</button><button onClick={() => scrollToCat(catList[5])}>Millie</button><button onClick={() => scrollToCat(catList[9])}>Bella</button></nav><div><ul>{catList.map((cat) => (<likey={cat}ref={(node) => {// 将这个getMap函数传入,这样DOM的ref就可以以map的形式操作const map = getMap();// 添加到 Map 中map.set(cat, node);// 从 Map 中移除return () => {map.delete(cat);};}}><img src={cat} /></li>))}</ul></div></>);
}function setupCatList() {const catList = [];for (let i = 0; i < 10; i++) {catList.push("https://loremflickr.com/320/240/cat?lock=" + i);}return catList;
}
访问另一个组件的 DOM 节点

有时候会有A组件操作B组件DOM节点的需求,比如在执行某些操作后,实现表单输入框的自动聚焦。但Ref 是一个脱围机制,也就是除了在迫不得已的情况下尽量别用。手动操作其它 组件的 DOM 节点可能会让代码变得脆弱。如果真的要用,可以看看这个例子。

import { useRef } from 'react';function MyInput({ ref }) {
// 子组件从props中获取ref,绑定在对应的DOM节点上return <input ref={ref} />;
}export default function MyForm() {
// 在父组件里声明refconst inputRef = useRef(null);function handleClick() {inputRef.current.focus();}return (<>// 把ref作为参数传到子组件中<MyInput ref={inputRef} /><button onClick={handleClick}>聚焦输入框</button></>);
}

这样做确实可以实现在A组件中调用B组件的DOM,但某些情况下,可能只需要调用B组件DOM的其中一些方法,比如在这个例子里只需要调用focus方法,但这样写会将DOM所有方法都给了MyForm组件。还有些更加极端的需求,A组件可能需要调用B组件中的某些方法,这个时候,可以使用useImperativeHandle来实现

import { useRef, useImperativeHandle } from "react";function MyInput({ ref }) {const realInputRef = useRef(null);// useImperativeHandle 指示 React 将你自己指定的对象作为父组件的 ref 值。 // 所以 Form 组件内的 inputRef.current 将只有 focus 方法。useImperativeHandle(ref, () => ({// 只暴露 focus,没有别的// ref在这里不是 DOM 节点,而是在 useImperativeHandle 调用中创建的自定义对象。所以除了DOM方法外,还可以将其他A组件需要调用的方法也一并传入focus() {realInputRef.current.focus();},someFun() {console.log('test')}}));return <input ref={realInputRef} />;
};export default function Form() {const inputRef = useRef(null);function handleClick() {inputRef.current.focus();}return (<><MyInput ref={inputRef} /><button onClick={handleClick}>聚焦输入框</button></>);
}
React 何时添加 refs

在 React 中,每次更新都分为 两个阶段:

  • 在 渲染 阶段, React 调用你的组件来确定屏幕上应该显示什么。
  • 在 提交 阶段, React 把变更应用于 DOM。
    在第一次渲染期间,DOM 节点尚未创建,因此 ref.current 将为 null。在渲染更新的过程中,DOM 节点还没有更新。所以读取它们还为时过早。
    React 在提交阶段设置 ref.current。在更新 DOM 之前,React 将受影响的 ref.current 值设置为 null。更新 DOM 后,React 立即将它们设置到相应的 DOM 节点。
    通常,你将从事件处理器访问 refs。 如果想使用 ref 执行某些操作,但没有特定的事件可以执行此操作,可能需要一个 effect。这就是后面的内容了。

彩蛋:用 flushSync 同步更新 state
请看下面这个代码,需要实现的是添加一个新的待办事项,并将屏幕向下滚动到列表的最后一个子项。请注意,出于某种原因,它总是滚动到最后一个添加之前的待办事项

import { useState, useRef } from 'react';export default function TodoList() {const listRef = useRef(null);const [text, setText] = useState('');const [todos, setTodos] = useState(initialTodos);function handleAdd() {const newTodo = { id: nextId++, text: text };setText('');setTodos([ ...todos, newTodo]);listRef.current.lastChild.scrollIntoView({behavior: 'smooth',block: 'nearest'});}return (<><button onClick={handleAdd}>添加</button><inputvalue={text}onChange={e => setText(e.target.value)}/><ul ref={listRef}>{todos.map(todo => (<li key={todo.id}>{todo.text}</li>))}</ul></>);
}let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {initialTodos.push({id: nextId++,text: '待办 #' + (i + 1)});
}

执行代码后会发现,原本想要滚动到最后新加的待办事项中,但实际上会滚到上一个事项,自动滚动无法定位到新添加的待办事项中。
问题出现在这两行代码中:

// 在 React 中,state 更新是排队进行的,setTodos 不会立即更新 DOM。
// 当ref操作scroll事件使得列表滚动到最后一个元素时,尚未添加待办事项
// 因此这里需要实现setTodos立即更新
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();// 可以使用react-dom中的flushSync来实现这个强制更新DOM的过程
import { flushSync } from 'react-dom';
// ...只展示关键代码
flushSync(() => {setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
使用 refs 操作 DOM 的最佳实践

还是反复强调的事情,Ref是一种脱围机制,所以必须只在需要跳出“React”范围的时候才能使用,否则如果胡乱修改DOM元素,一旦跟React自身的渲染机制冲突了,就容易造成不可预期的后果。
因此,需要避免更改由 React 管理的 DOM 节点。 对 React 管理的元素进行修改、添加子元素、从中删除子元素会导致不一致的视觉结果,或造成代码崩溃。总之就是,不是不能改,而是改的时候需要小心些。

ref的场景就学完了,接下来是Effect的模块,这个模块比较长,就单独再开一篇来讲好了~

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

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

相关文章

去除Edge微软浏览器与Chrome谷歌浏览器顶部出现“此版本的Windows不再支持升级Windows 10”的烦人提示

前言 在 Windows 7 中&#xff0c;安装 Microsoft Edge 109 版本后&#xff0c;启动浏览器时会弹出提示&#xff1a; 此版本的 Windows 不再支持 Microsoft Edge。升级到 Windows 10 或更高版本&#xff0c;以获取常规功能和安全更新。 同样地&#xff0c;安装 Google Chrome 1…

PWM、脉冲

要求&#xff1a;一、PWM输出PWM波生成原理在此处使用TIM2生成PWM&#xff0c;PA1输出PWM波。CNT小于CCR时&#xff0c;输出高电平&#xff1b;CNT大于CCR时&#xff0c;输出低电平。 输入捕获测量频率的原理输入捕获的捕获意思是它在PWM波上升沿或者下降沿的时候&#xff0c;会…

文件IO(1)

.文件IO1.概念标准IO是有缓存的IO&#xff0c;文件IO没有缓存&#xff0c;适合于通信、硬件设备操作标准IO是库函数&#xff0c;文件IO是系统调用2.系统调用与库函数系统调用&#xff1a;是Linux内核中的代码&#xff0c;只能在Linux系统中使用库函数&#xff1a;是对系统调用的…

【AI】Pycharm中要注意Python程序文件的位置

博主试着在本地电脑用Pycharm环境运行随便一个机器学习然后做图像识别的模型&#xff0c;Python的程序一直报博主学习图片的路径不正确&#xff0c;博主查了好几遍&#xff0c;也没找出问题&#xff0c;后来借助Deepseek才知道&#xff0c;Python主程序的位置一定要在Project下…

TDengine 可观测性最佳实践

TDengine 介绍 TDengine 是一款开源、高性能、云原生的时序数据库&#xff0c;专为物联网、车联网、工业互联网、金融、IT 运维等场景优化设计。它不仅提供了高效的数据存储和查询功能&#xff0c;还带有内建的缓存、流式计算、数据订阅等系统功能&#xff0c;能大幅减少系统设…

Jenkins 搭建鸿蒙打包

1、创建流水线工程 选择 Freestyle project 2、配置模板仓库、凭证 配置仓库地址 创建凭证&#xff0c;凭证选择账号-密码&#xff08;能够访问该仓库的个人或管理员 Gitlab 账密&#xff09; 到这里执行构建&#xff0c;便可以克隆仓库到工作目录 3、安装插件 3.1 Rebuild…

【SpringBoot】02 基础入门-什么是Spring Boot?:Spring与SpringBoot

文章目录1、Spring能做什么1.1、Spring的能力1.2、Spring的生态1.3、Spring5重大升级1.3.1、响应式编程1.3.2、内部源码设计2、为什么用SpringBoot2.1、SpringBoot优点2.2、SpringBoot缺点3、时代背景3.2、分布式分布式的困难分布式的解决3.3、云原生上云的困难4、如何学习Spri…

FFmpeg 编译安装和静态安装

FFmpeg 编译安装和静态安装 简介 FFmpeg 是一个领先的多媒体框架&#xff0c;能够解码、编码、转码、复用、解复用、流化、过滤和播放几乎所有人类和机器创建的格式。本指南将详细介绍如何在 CentOS 8.5.2111 系统上从源代码编译并安装 FFmpeg 6.1.1 版本。从源代码编译安装可…

人大BABEC地平线高效率具身导航!Aux-Think:探索视觉语言导航中数据高效的推理策略

作者&#xff1a; Shuo Wang1,3^{1,3}1,3, Yongcai Wang1^{1}1, Wanting Li1^{1}1 , Xudong Cai1^{1}1, Yucheng Wang3^{3}3, Maiyue Chen3^{3}3, Kaihui Wang3^{3}3, Zhizhong Su3^{3}3, Deying Li1^{1}1, Zhaoxin Fan2^{2}2单位&#xff1a;1^{1}1中国人民大学&#xff0c;2^…

01. maven的下载与配置

1.maven的下载与初步配置a.下载并配置仓库地址下载maven压缩包&#xff0c;并解压&#xff0c;解压后应有如下几个文件点击conf&#xff0c;打开settings.xml&#xff08;我用的VScode打开的&#xff09;&#xff0c;我们需要声明一下内部仓库的地址&#xff0c;以及私服的一些…

1701. 请输出所有的3位对称数

问题描述请输出所有的 33 位对称数&#xff0c;对称数指的是一个整数 nn 正过来和倒过来是一样的&#xff0c;比如&#xff1a;101、121、282…101、121、282…请从小到大输出符合条件的3位对称数&#xff0c;每行 11 个。输入无。输出从小到大按题意输出符合条件的数&#xff…

C++算法·排序

排序的定义 这个不用说吧 就是根据某个条件对一个数列进行有序的操作 例如要求从小到大排序、从大到小排序等等 排序的分类 比较排序(Comparison(Comparison(Comparison Sorts)Sorts)Sorts) 特点&#xff1a;通过元素间的比较决定顺序 时间复杂度下限&#xff1a;O(nO(nO(n…

微服务项目中的注册中心——Nacos配置

从零开始&#xff1a;Nacos服务注册与配置中心实战教程 Nacos&#xff08;Dynamic Naming and Configuration Service&#xff09;是阿里巴巴开源的服务发现、配置管理工具&#xff0c;集注册中心与配置中心于一体&#xff0c;广泛应用于微服务架构。本文将从环境搭建到实战配…

日期格式化成英文月,必須指定語言環境

如果不指定Locale.ENGLISH 在有些JDK下 輸出6月 INV USD 314,791.77,DUE 25-07 [PAID USD 503,389.56 ON 2025-07-16]Mar INV USD 52,042.00,DUE 25-07 [PAID USD 52,042.00 ON 2025-08-11]所以必…

【6】Transformers快速入门:Transformer 的注意力层 是啥?

一句话看懂注意力层作用&#xff1a;让 AI 像人一样 “抓重点” &#xff08;比如读“猫追老鼠”&#xff0c;自动聚焦 “追” 这个动作&#xff0c;忽略无关词&#xff09;1. 为什么需要注意力&#xff1f; 问题场景&#xff08;翻译例子&#xff09;&#xff1a; 英文&#x…

集合,完整扩展

目录 前言&#xff1a; 一、List接口 1.1 ArrayList 1.2 LinkedList 1.3 Vector 二、Set接口 2.1 HashSet 2.2 TreeSet 2.3 LinkedHashSet 三、应用选择 前言&#xff1a; 本篇文章重点梳理 List 接口和 Set 接口的核心内容&#xff0c;结合代码案例帮大家吃透它们的…

【doris基础与进阶】3-Doris安装与部署

安装前的准备 在windows系统上通过vmwareubuntu 22.04的方式进行安装&#xff0c;由于资源有限&#xff0c;在同1台机器上同时安装fe和be&#xff08;broker本次不安装&#xff0c;极简化安装&#xff09;&#xff0c;安装版本为2.1.10&#xff0c;2.x版本架构不会有大的变化&a…

关于数据结构6-哈希表和5种排序算法

哈希表1哈希算法将数据通过哈希算法映射成一个键值&#xff0c;存取都在同一个位置实现数据的高效存储和查找&#xff0c;将时间复杂度尽可能降低至O(1)2哈希碰撞多个数据通过哈希算法得到的键值相同&#xff0c;成为产生哈希碰撞3哈希表&#xff1a;构建哈希表存放0-100之间的…

AWT与Swing深度对比:架构差异、迁移实战与性能优化

全面对比分析Java AWT与Swing GUI框架的架构差异、性能表现和适用场景&#xff0c;提供完整的AWT到Swing迁移实战指南&#xff0c;包含15代码示例、性能测试数据、最佳实践建议&#xff0c;助你做出明智的技术选型和实现平滑迁移。 Java AWT, Swing, GUI框架对比, 代码迁移, 性…

git仓库检测工具

介绍 Gitleaks 是一款用于检测git 仓库、文件以及任何你想通过 git 传递的信息(例如密码、API 密钥和令牌)的工具stdin。如果你想了解更多关于检测引擎工作原理的信息,请查看这篇博客:正则表达式(几乎)就是你所需要的一切。 ➜ ~/code(master) gitleaks git -v○│╲│…