脱围:使用 ref 保存值及操作DOM

news2024/10/5 14:37:17

♻️ 前面多篇文章中提及:state 可以 ① 保存渲染间的数据; ② state setter 函数更新变量会触发 React 重新渲染组件。
在这里插入图片描述

// 子组件:显示当前时间
function Time() {
    return (
        <p>{new Date().toLocaleString()}</p>
    )
}

export default () => {
    const [counter, setCounter] = useState(0);
    return (<>
        <span>{counter}</span>
        <button onClick={() => setCounter(counter + 1)}>+</button>
        <Time />
    </>)
}

默认情况下,当一个组件重新渲染时,React 会递归地重新渲染它的所有子组件

每一次点击按钮, counter + 1 ,都会导致整个组件渲染(包括 <Time>),因此总是显示当前时间。

触发
组件初始化
渲染
点击按钮
state修改
渲染

如何使得 state 每次加 1,但子组件 <Time> 不变 ?

方式一:子组件使用 state

该方式:只修改子组件

function Time() {
    let [time, setTime] = useState(new Date().toLocaleString());
    return (
        <p>{time}</p>
    )
}

在这里插入图片描述
点击按钮,counter + 1,但 <Time> 组件不被重新渲染,保持第一次的值。

⚠️ 相同位置的相同组件会使得 state 被保留下来! 具体可见「续篇:展开聊下 state 与 渲染树中位置的关系」

方式二:子组件使用 memo 包裹

该方式:只修改子组件

const Time = memo(() => {
    return (
        <p>{new Date().toLocaleString()}</p>
    )
})

实现效果同上述「方式一」。

通过此更改, <Time> 的所有 props 都与上次渲染时相同(这里都为空), <Time> 跳过重新渲染。关于useMemo 可参阅官网 1

⚓ 方式三:父组件使用 ref

该方式:只修改父组件

export default () => {
    const counterRef = useRef(0);

    return (<>
        <span>{counterRef.current}</span>
        <button onClick={() => counterRef.current = counterRef.current + 1}>+</button>
        <p>{new Date().toLocaleString()}</p>
    </>)
}

在这里插入图片描述
同上述「方式一」&「方式二」的差异:当前DOM不发生任何变化(依然为0,其 counterRef.current 的值已经变成了 1)。

不会触发渲染
组件初始化
渲染
点击按钮
ref修改
结束

当希望组件“记住”数据,又不想触发新的渲染时,便可以使用 ref

ref 是一种脱围机制2,用于保留不用于渲染的值:有些组件可能需要控制和同步 React 之外的系统。

例如,可能需要使用浏览器 API 聚焦输入框,或者在没有 React 的情况下实现视频播放器,或者连接并监听远程服务器的消息。

  • 存储 timeout ID
  • 存储和操作 DOM 元素
  • 存储不需要被用来计算 JSX 的其他对象

ref 与 state 不同之处

✈️ 与 state 一样,React 会在每次重新渲染之间保留 ref。但是,设置 state 会重新渲染组件,更改 ref 不会 !

refstate
useRef(initialValue)返回 { current: initialValue }useState(initialValue) 返回 state 变量的当前值和一个 state 设置函数 ( [value, setValue])
更改时不会触发重新渲染更改时触发重新渲染。
可变 —— 可以在渲染过程之外修改和更新 current 的值。“不可变” —— 必须使用 state 设置函数来修改 state 变量,从而排队重新渲染。
不应在渲染期间读取(或写入) current 值。可以随时读取 state。但是,每次渲染都有自己不变的 state 快照。

useRef 内部是如何运行的?3

// 原则上 useRef 可以在 useState 的基础上 实现
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}
注意:不要在渲染过程中读取或写入 ref.current

如果渲染过程中需要某些信息,请使用 state 代替。【如上述方案三,点击按钮并未渲染最新的值】

function Test1 () {
    let [counter, setCounter] = useState(0);
    return (<>
        <p>{counter}</p>
        <button onClick={() => {
            setCounter(n => n + 1);
            console.log(counter);
        }}>点我</button>
    </>)
}

function Test2 () {
    let counterRef = useRef(0);
    return (<>
        <p>{counterRef.current}</p>
        <button onClick={() => {
            counterRef.current++;
            console.log(counterRef.current);
        }}>点我</button>
    </>)
}
数据 state显示 <p>{counter}</p>数据 ref.current显示 <p>{counterRef.current}</p>
第1次点击0110
第2次点击1220
第3次点击2330

ref 本身是一个普通的 JavaScript 对象,具有一个名为 current 的属性,可以对其进行读取或设置

由于 React 不知道 ref.current 何时发生变化,即使在渲染时读取它也会使组件的行为难以预测。

获取指向节点的 ref
export default () => {
	const inputRef = useRef(null);
	return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current.focus()}>
        聚焦输入框
      </button>
    </>
  );
}
获取列表节点的 ref

当 ref 数量不确定(如列表),需要为每一项都绑定 ref。

在这里插入图片描述

方案一:用一个 ref 引用其父元素,然后用 DOM 操作方法(如 querySelectorAll)来寻找子节点。该方案比较脆弱,当 DOM 结构发生变化,则会失效或报错。

✅方案二:将函数传递给 ref 属性,ref 回调4当需要设置 ref 时,React 将传入 DOM 节点来调用你的 ref 回调,并在需要清除它时传入 null 。这使你可以维护自己的数组或 Map,并通过其索引或某种类型的 ID 访问任何 ref。

/* 动态添加 input 元素,并让最新添加的 input 元素获取焦点 */
const List = () => {
    const [data, setData] = useState<string[]>([])

    const inputsRef = useRef<Map<number, HTMLElement>>(null);
    function getMap(){
        if (!inputsRef.current) {
            // 首次运行时,初始化 map
            inputsRef.current = new Map();
        }
        return inputsRef.current
    }

    return (<>
        <ul style={{display: 'flex', flexDirection: 'column'}}>
            {data.map((item, index) => {
                return (
                    <li key={index}>
                        <input 
                         ref={(node) => {
                            const map = getMap();
                            // 存在追加,null删除
                            node ? map.set(index, node) : map.delete(index);
                        }}
                        type="text" 
                        value={item} 
                        onChange={console.log}/>
                    </li>
                )
            })}
        </ul>
        <button onClick={() => {
            flushSync(() => {
                setData([...data, `${Date.now()}`]);
            });
            const map = getMap();
            // 获取焦点
            map.get(data.length)?.focus();
        }}>添加</button>
    </>)
}

setData([...data, Date.now()]); 不会立即更新 DOM(state 更新是排队进行的),这里使用 flushSync(() => { ... }) 强制 React 同步更新(“刷新”)DOM。

获取自定义组件的 ref

将 ref 放在像 <input /> 这样输出浏览器元素的内置组件上时,React 会将该 ref 的 current 属性设置为相应的 DOM 节点。

默认情况下,自定义组件不会暴露它们内部 DOM 节点的 ref。

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

// forwardRef 允许组件使用 ref 将 DOM 节点暴露给父组件(父组件中按常规方式引用)
const MyInput = forwardRef((props, ref) => {
	return <input {...props} ref={ref} />;
});

延伸: 子组件内部可以使用 useImperativeHandle5 限制暴漏的功能。

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // 只暴露 focus,没有别的
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} ref={realInputRef} />;
});

总结

ref 是一种脱围机制,用于保留不用于渲染的值。同时,ref 是一个普通的 JavaScript 对象,具有一个名为 current 的属性,可以对其进行读取或设置。与 state 不同,设置 ref 的 current不会触发重新渲染不要在渲染过程中读取或写入 ref.current。这使组件难以预测。


  1. https://react.docschina.org/reference/react/useMemo useMemo ↩︎

  2. https://react.docschina.org/learn/escape-hatches 脱围机制 ↩︎

  3. https://react.docschina.org/learn/referencing-values-with-refs#how-does-use-ref-work-inside useRef 内部是如何运行的? ↩︎

  4. https://react.docschina.org/reference/react-dom/components/common#ref-callback ref 回调函数 ↩︎

  5. https://react.docschina.org/reference/react/useImperativeHandle useImperativeHandle ↩︎

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

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

相关文章

伪分布HBase的安装与部署

1.实训目标 &#xff08;1&#xff09;熟悉掌握使用在Linux下安装伪分布式HBase。 &#xff08;2&#xff09;熟悉掌握使用在HBase伪分布式下使用自带Zookeeper。 2.实训环境 环境 版本 说明 Windows 10系统 64位 操作电脑配置 VMware 15 用于搭建所需虚拟机Linux系统 …

蜂窝物联:智慧水产养殖解决方案

一、系统介绍 集约化水产养殖水质在线监控系统是面向水产养殖集约、高产、高效、生态、安全的发展需求&#xff0c;基于智能传感、无线传感网、通信、智能处理与智能控制等物联网技术开发的&#xff0c;集水质环境参数在线采集、无线传输、智能处理、预警信息发布、决策支持、远…

鸿蒙Harmony应用开发—ArkTS声明式开发(基础手势:Navigation)

Navigation组件是路由导航的根视图容器&#xff0c;一般作为Page页面的根容器使用&#xff0c;其内部默认包含了标题栏、内容区和工具栏&#xff0c;其中内容区默认首页显示导航内容&#xff08;Navigation的子组件&#xff09;或非首页显示&#xff08;NavDestination的子组件…

[虚拟机]

如果你电脑的物理机器硬件强大, 由于一台物理机器只能运行一个操作系统, 那么就会造成物理机器硬件的浪费 虚拟机:使用虚拟化技术&#xff0c;将一台物理机器虑拟化为多台虚拟机器&#xff08;Virtual Machine, VM)&#xff0c;每个虚拟机器都可以独立运行一个操作系统 虚拟机…

一次直播和图像识别技术应用的探索之旅

背景 Think Better 很多行业都在“卷”&#xff0c;作为金融科技行业的信也&#xff0c;也不例外。除了卷云计算、大数据和人工智能这些非常有深度的技术以外&#xff0c;信也向着技术融合创新的方向逐步探索&#xff0c;做得更好一些&#xff0c;为用户提供更好的价值&#xf…

【“双碳”目标】Acrel-2000Z分布式光伏发电监测系统解决方案

1 概述 “十四五”期间&#xff0c;随着“双碳”目标提出及逐步落实&#xff0c;本就呈现出较好发展势头的分布式光伏发展有望大幅提速。就“十四五”光伏发展规划&#xff0c;国家发改委能源研究所可再生能源发展中心副主任陶冶表示&#xff0c;“双碳”目标意味着国家产业结…

Django高级之-cookie-session-token

Django高级之-cookie-session-token 发展史 1、很久很久以前&#xff0c;Web 基本上就是文档的浏览而已&#xff0c; 既然是浏览&#xff0c;作为服务器&#xff0c; 不需要记录谁在某一段时间里都浏览了什么文档&#xff0c;每次请求都是一个新的HTTP协议&#xff0c; 就是请…

20240306数据库的毫秒级上锁特性与可重复读级别的MVCC快照处理方式

从后端程序发送SELECT指令到数据库加上读锁&#xff0c;这个过程的时间差取决于多个因素&#xff0c;包括网络延迟&#xff08;如果存在&#xff09;、数据库服务器的处理能力、当前数据库的负载以及查询本身的复杂度。在你的情况下&#xff0c;由于后端程序和数据库服务器运行…

Intel@cpu产品参数和命名@单核睿频和全核睿频

文章目录 选择合适的cpuintel cpu型号和命名小结 cpu排行时钟速度睿频单核睿频和全核睿频网络上流传的方法 在线查询 产品比较跑分比较 选择合适的cpu 如何选择游戏 CPU - 英特尔 (intel.cn)在决定购买具体的产品之前,建议广泛地查阅用户对它的评价以及是否有哪些因素是不满足…

AI时代,AI智能交互数字人赋能公共服务降本增效

人工智能时代&#xff0c;AI交互数字人技术不断在冲击公共服务领域。越来越多公共服务领域开始在自身业务中运用AI智能交互数字人&#xff0c;通过布局AI交互数字人应用于代言人、推荐官、客服、主播等诸多领域。 近年来&#xff0c;数字人技术正在成为引领数字化时代营销的重…

常用云产品连接

阿里云常用云产品 云服务器 阿里云&#xff1a;云服务器ECS_云主机_服务器托管_计算-阿里云 对象存储 阿里云&#xff1a;对象存储 OSS_云存储服务_企业数据管理_存储-阿里云 短信服务 阿里云&#xff1a;短信服务_企业短信营销推广_验证码通知-阿里云 CDN服务 阿里云&…

mysql日常优化的总结

文章目录 一、数据表结构相关优化建字段类型注意事项1. int类型的选择2.varchar、char、text类型3.date、datetime、timestamp类型 表规划1. 垂直分表2. 水平分表 二、查询语句优化1.对于字段多的表&#xff0c;避免使用SELECT *2.避免使用!操作符3.避免使用null做条件4.like查…

李彦宏:程序员会消失;拜登谈TikTok撤离;李想终就MEGA风波表态;英伟达紧急处理全球范围故障;刘作虎:AI手机非噱头

一、商业圈 1.阿里包揽中国估值最高的5家大模型独角兽 在对AI大模型初创企业的投资竞赛中&#xff0c;阿里巴巴已成为中国互联网巨头中最大的投资者目前&#xff0c;中国一级市场上估值最高的5家A大模型初创公司&#xff0c;阿里均参与投资&#xff0c;并在2-3家公司中可能是…

pip 和conda 更换镜像源介绍

1、前言 很多深度学习的项目免不了安装库文件、配置环境等等&#xff0c;如果利用官方提供的连接&#xff0c;网速很慢&#xff0c;而且很容易download掉。 所以配置好了虚拟环境&#xff0c;将pip换源属实重要 常见的国内镜像源有清华、中科大、阿里等等... 这里建议用中科…

抖音视频提取gif怎么做?分分钟帮你生成gif

通过将视频转换成gif动图的方式能够方便的在各种平台上分享、传播。相较于视频文件&#xff0c;gif动图的体积更小&#xff0c;传播起来更方便&#xff0c;能够吸引大众的注意力。下面&#xff0c;就来给大家分享一个gif图片制作&#xff08;https://www.gif.cn/&#xff09;的…

记录一个编译的LLVM 含clang 和 PTX 来支持 HIPIFY 的构建配置

llvm 18 debug 版本 build llvmorg-18.1rc4 debug $ cd llvm-project $ git checkout llvmorg-18.1.0-rc4 $ mkdir build_d $ cd build_d $ mkdir -p ../../local_d cmake \ -DCMAKE_INSTALL_PREFIX../../local_d \ -DLLVM_SOURCE_DIR../llvm \ -DLLVM_ENABLE_PROJECTS&…

WorkPlus Meet提供高效、安全视频会议解决方案

WorkPlus Meet是一款私有部署和定制化的视频会议解决方案&#xff0c;为企业提供高效、安全的远程协作平台。随着全球数字化转型的加速&#xff0c;视频会议已成为企业必不可少的工作工具&#xff0c;而WorkPlus Meet的私有部署和定制化功能&#xff0c;为企业提供了更大的控制…

【leetcode热题】排序链表

给你链表的头结点 head &#xff0c;请将其按 升序 排列并返回 排序后的链表 。 示例 1&#xff1a; 输入&#xff1a;head [4,2,1,3] 输出&#xff1a;[1,2,3,4]示例 2&#xff1a; 输入&#xff1a;head [-1,5,3,4,0] 输出&#xff1a;[-1,0,3,4,5]示例 3&#xff1a; 输入…

14:00面试,14:10就出来了,问的问题有点变态。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到3月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%,…

有没有能用蓝牙的游泳耳机?四款好评如潮的游泳耳机速来围观

在如今的快节奏生活中&#xff0c;音乐已经成为了我们日常休闲和运动时的重要伴侣。尤其对于水上运动爱好者来说&#xff0c;一款好的游泳耳机更是必不可少。它不仅能让我们在水中畅享音乐&#xff0c;还能有效隔绝外界噪音&#xff0c;让我们更加专注于自己的运动。 &#xf…