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

重学React(五):脱围机制一

背景: 之前将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.lryc.cn/news/617449.html

相关文章:

  • 金蝶云星辰:赋能企业数据管理
  • spring boot 整合redis教程
  • 带简易后台管理的米表系统 域名出售系统 自适应页面
  • 帝国理工学院团队研发:Missense3D-PTMdb—— 解析遗传变异与翻译后修饰的交互式工具
  • 计算机网络---交换机
  • 套接字技术、视频加载技术、断点续传技术
  • Horse3D引擎研发笔记(四):在QtOpenGL下仿three.js,封装EBO绘制四边形
  • 2025 年国内可用 Docker 镜像加速器地址
  • Rust面试题及详细答案120道(19-26)-- 所有权与借用
  • 《基于Pytorch实现的声音分类 :网页解读》
  • YOLOv8 训练报错:PyTorch 2.6+ 模型加载兼容性问题解决
  • 【JavaEE】(12) 创建一个 Sring Boot 项目
  • 第二届机电一体化、机器人与控制系统国际会议(MRCS 2025)
  • 34-Hive SQL DML语法之查询数据-3
  • 2025世界机器人大会,多形态机器人开启商业化落地浪潮
  • [4.2-2] NCCL新版本的register如何实现的?
  • GAI 与 Tesla 机器人的具体联动机制
  • 记录一下通过STC的ISP软件修改stc32的EEPROM值大小
  • VoxCraft-生数科技推出的免费3D模型AI生成工具
  • uni-app app端安卓和ios如何申请麦克风权限,唤起提醒弹框
  • 设计模式笔记_结构型_组合模式
  • 5G NTN 卫星测试产品
  • 5G NR 非地面网络 (NTN) 5G、太空和统一网络
  • 用Python实现Excel转PDF并去除Spire.XLS水印
  • 深度剖析 Linux 信号:从基础概念到高级应用,全面解析其在进程管理与系统交互中的核心作用与底层运行机制
  • 电力仿真系统:技术革新与市场格局的深度解析
  • 【CV 目标检测】①——目标检测概述
  • 【Oracle】如何使用DBCA工具删除数据库?
  • 低延迟RTSP|RTMP视频链路在AI驱动无人机与机器人操控中的架构实践与性能优化
  • 排序与查找,简略版