浅谈React Refs 使用场景及核心要点

目录
  • 什么是 Refs?
  • 使用方式
  • Refs 核心要点
    • 避免重复创建 ref 内容
    • ref.current 存储的内容修改是突变
    • ref 作为数据存储时内容的变化不会引起 re-render
    • ref 的读写只能在 useEffect 或者回调函数中进行
    • 跨组件传递ref 获取dom时需要借助 forwardRef 包裹组件
    • ref 绑定的dom在离屏或者未挂载时ref.current 值会被修改为null
  • 最佳实践
    • dom 操作相关
    • 用于在两次 render 之间传递数据

在使用 React 进行开发过程中,或多或少使用过 Refs 进行 DOM 操作或者访问一些DOM上的API,又或使用 Refs 保存数据。不管怎么说 Refs 总是 React 提供的一大助力,这篇文章主要介绍 Refs 功能和使用场景以及注意事项。希望能增强对 Refs 的理解,掌握好这把利剑。

什么是 Refs?

Refs 是 React 提供的用来保存 object 引用的一个解决方案,在函数式组件使用 useRef 创建一个 ref 对象,ref 对象存在一个可直接修改的 current 属性,内容都是存在 current 上。Refs 使用场景主要分为两个方向,其一是实现 DOM 访问与操控在两次render之间传递数据内容【和state机制有很大不同,下文会有对比介绍】。如果在组件返回的 jsx dom上绑定了 ref 属性,React 在处理 jsx 时会把该dom节点【原生node节点】的引用存储在 ref.current 上。

使用方式

分为三步:

  • 第一步、使用 useRef 创建 ref 对象(useRef 是 FC hooks, class 组件使用 React.createRef() 创建 )
  • 第二步、赋值&使用【操作dom则绑定为dom的ref属性的值,用于保存值的时候传递内容给 ref.current】,
  • 第三步、访问ref内容【进行dom对应的api访问,进行 scroll 、focus等操作。又或者从current中读取保存的数据】最终的目的还是最后访问拿到对应的数据进行操作。下边我们分别用两个小 demo 简单先看看用法,理论和总结在后边一点。

Example one 实现点击按钮 focus input 框

import React, { useRef } from "react";

export default function Comp() {
  // 第一步:使用 useRef 创建一个 ref 对象 { current: null }
  const ref = useRef();

  function handleClick() {
    // 第三步:访问到 ref 上存的内容,这里是 input 的node节点
    ref.current.focus();
  }

  // 第二步:赋值 ref
  return (
    <>
      <input ref={ref} />
      <button onClick={handleClick}>开始输入</button>
    </>
  );
}

Example two 实现数据发送3s内撤回功能:在点击发送后3s内如果点击 “取消发送” 则取消本次发送

简单起见我们按钮不实际发送请求,定时 3s 如果3s内点击了 “取消发送”则取消发送。发送功能用提醒 “已发送”代替,出现“已发送”表示执行了发送。

import React, { useRef, useState } from "react";

export default function CompA() {
  // 第一步:使用useRef 创建 ref 对象
  const ref = useRef();
  const [isSending, setIsSending] = useState(false);

  function send() {
    // ...
    window.alert("消息已发送!");
    setIsSending(false);
  }

  function undo() {
    // 第三步: 访问存在 ref 上的 timeout ID, 进行定时取消
    clearTimeout(ref.current);
  }

  function handleClickSendBtn() {
    setIsSending(true);
    // 第二步: 赋值,将 timeout ID 存在 ref 上
    ref.current = setTimeout(send, 3000);
  }

  function handleClickCancelBtn() {
    undo();
    setIsSending(false);
  }

  return (
    <>
      <button onClick={handleClickSendBtn} disabled={isSending}>
        {isSending ? "发送中..." : "发送"}
      </button>
      {isSending && <button onClick={handleClickCancelBtn}>取消发送</button>}
    </>
  );
}

Refs 核心要点

我们通过两个简单 case 演示了一下,DOM 操作 以及用于在两次 re-render 之间传递内容(case 2 传递的内容是 timeout 的ID)。在使用 Refs 的过程中有几点尤其需要注意。

避免重复创建 ref 内容

在使用 useRef 进行创建 ref 时可以传递 null、number 、object 等内容也可以传递初始化函数。React 只会保存一次初始值,并把它带到下一次 render 中。因此在 useRef 在创建ref的时候传递重复的内容是不生效的,如果你认为每次都生成一个新的值赋给ref但是React给你的却是第一次传递的值,这可能不符合你的预期。

import React, { useEffect, useRef, useState } from "react";

export function CompB() {
  // 注意: 这个部分不会每次生成一个新的时间戳,只会采用 mounted 时新建的第一次时间戳。
  const ref = useRef(+new Date());

  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`第${count}渲染时间:`, +new Date());
    console.log("ref", ref.current);
  });

  function handleClick() {
    // 为了让点击时,更新 state 触发 re-render
    setCount(count + 1);
  }

  return (
    <>
      <button onClick={handleClick}>点击让组件渲染</button>
    </>
  );
}

注意:效果图中初始化的时候会打印两次重复的第0次渲染,是因为 React 在 dev 模式下会执行两遍组件内容,检测组件是否是纯组件。并非代码问题,后续研读源码时会有文章介绍,欢迎关注。

ref.current 存储的内容修改是突变

对于 state 来说,直接修改state不会生效。需要使用 useState 给的第二个返回值来进行修改。而 ref 则是可直接修改 current 属性上的内容,并且修改后可以立即取到值。ref存储的实际就是一个引用,因此是可突变的。

import React, { useRef } from 'react';
export function Comp() {
  const ref = useRef(0);

  useEffect(()=>{
    console.log(ref.current); // 0
    // 突变
    ref.current = ref.current + 1;
    console.log(ref.current); // 1
  });

  return <div></div>
}

ref 作为数据存储时内容的变化不会引起 re-render

React 组件的 re-render 的触发一般是【state、props、context】中的出现变化引起的。修改 Ref 的内容不会引起组件的 re-render 因此不能用 ref 去干预 React 生成jsx。换句话说就是不能用在jsx中做渲染或者条件判断,不然可能得到没办法预料的jsx结果。

ref 的读写只能在 useEffect 或者回调函数中进行

React 约定 state、props、context 都是一样的就应该输出同样的jsx内容,只要这三个要素不变那么以不同的调用顺序执行组件应该得到同样的结果。要说清楚为什么Ref的读写只能在useEffect和回调函数中,得先铺垫一下React的一些架构知识。

React 架构上分为三个部分【调度器Scheduler、协调器Reconciler、渲染器Renderer】,整体上又是两个阶段【render 阶段,commit阶段】。render 阶段的目的是找出哪些组件需要更新,以及如何更新(这些内容会标记在Fiber节点上)【更新过程可中断可抢占的,高优的更新可抢占优先先执行。这个阶段主要是 Scheduler 负责调度优先级, 协调器负责找出更新的内容并标记好】,commit 阶段的作用用一句话就是【根据 render 阶段标记的结果Fiber上的tag,操作dom,执行 useEffect 以及对应阶段的生命周期函数】。

在 render 阶段会执行组件,如果出现高优更新抢占,那么低优先级的更新在高优更新执行完成后会重新执行一遍【函数式组件也就是个function函数,在函数体中间的执行 ref 写操作会被多次执行】,我们会发现如果ref的赋值操作在这个期间执行了那么组件更新的结果就是不可预期的【未被抢占时ref的结果是1,被抢占1次时是2。这完全是不可预期大的】。而 useEffect 或者回调函数都不是在 render 阶段执行的因此每次更新只执行一次。也就是说ref的读写不能出现在render阶段,就只能写在 useEffect【类组件对应的是生命周期函数,注意不能写在 componentWillxxx 生命周期中,因为 componentWillxxx 生命周期函数执行在 render 阶段】和回调函数中

// bad
function MyComponent() {
  // ...
  //  Don't write a ref during rendering
  myRef.current = 123;
  // ...
  //  Don't read a ref during rendering
  return <h1>{myOtherRef.current}</h1>;
}

// good
function MyComponent() {
  // ...
  useEffect(() => {
    //  You can read or write refs in effects
    myRef.current = 123;
  });
  // ...
  function handleClick() {
    //  You can read or write refs in event handlers
    doSomething(myOtherRef.current);
  }
  // ...
}

跨组件传递ref 获取dom时需要借助 forwardRef 包裹组件

React 默认情况下不允许组件访问其他组件的dom节点,因此关闭了直接 props 传递 ref 标记组件的dom这种操作。得借助 React.forwardRef api 传递 实现这种跨组件的dom操作。

import React, { useEffect, useRef, useState, forwardRef } from "react";

export function ParentComp() {
  const childInputRef = useRef(null);

  function handleClick() {
    childInputRef.current?.focus();
  }

  return (
    <>
      <button onClick={handleClick}>编辑</button>
      <ChildComp ref={childInputRef} />
    </>
  );
}

// 使用forwordRef 包裹组件,接受 ref 并转发绑定到对应dom上
const ChildComp = forwardRef((props, ref) => {
  return (
    <div>
      <input {...props} ref={ref} />
    </div>
  );
});

ref 绑定的dom在离屏或者未挂载时ref.current 值会被修改为null

ref 绑定的dom在离屏或者未挂载时ref.current 值会被修改为 null 。如果在组件中间会进行条件渲染,那么需要处理一下判断逻辑,不然代码可能会抛出异常。另外在父组件引用子组件 dom 的场景也应该增加对 null 的判断。至此 Refs 的要点已经介绍完成。

最佳实践

接下来我们接着聊聊什么情况下使用 Refs 比较好,React 官方把 Refs 定义为逃生通道,就是暗示要谨慎使用。

dom 操作相关

  • 如果需要进行焦点管理位置滚动等非破坏性行为以及调用 dom 节点的 api 那么推荐使用 Refs。
  • 如果是为了修改 dom ,比如修改dom属性,标签名称等等,可能会与 React 存在冲突,不推荐这样使用Refs 而是应该换种思路考虑使用 state 进行条件渲染。

用于在两次 render 之间传递数据

  • 如果组件中大部分功能都依赖该数据,那么不应该存放在ref中。
  • 如果数据在jsx中使用,那么不推荐放在ref中, 这会带来问题【详见:ref 的读写只能在 useEffect 或者回调函数中进行】,推荐使用 useState。
  • 想要保存数据并且不希望数据变化时引起组件 re-render, 而只是在回调函数中需要获取到对应内容时,推荐使用 Ref。如 interval id 。

到此这篇关于浅谈React Refs 使用场景及核心要点的文章就介绍到这了,更多相关React Refs 使用场景 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入浅析React refs的简介

    一.是什么 Refs在计算机中称为弹性文件系统(英语:Resilient File System,简称ReFS) React中的Refs提供了一种方式,允许我们访问DOM节点或在render方法中创建的React元素 本质为ReactDOM.render()返回的组件实例,如果是渲染组件则返回的是组件实例,如果渲染dom则返回的是具体的dom节点 二.如何使用 创建ref的形式有三种: 传入字符串,使用时通过 this.refs.传入的字符串的格式获取对应的元素 传入对象,对象是通过 React

  • React中的Refs属性你来了解吗

    目录 1介绍 2使用方法 2.1createRef(版本>=React16.3) 2.2回调Refs 2.3String类型的Refs(已过时,不推荐使用) 2.4useRef(ReactHooks) 总结 1 介绍 react组件的三大属性: 1.props属性:封装传递给组件的参数 2.state属性:组件的状态,当值发生改变后,重新渲染组件 3.refs属性: (1)通过该属性可以去访问DOM元素或render函数中的react元素(虚拟的DOM元素) ——DOM元素或render函数中的

  • React中refs的一些常见用法汇总

    目录 什么是Refs 一.String 类型的 Refs 二.回调 Refs 三.React.createRef() 四.useRef 五.Refs 与函数组件 总结 什么是Refs Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素. Ref转发是一项将ref自动通过组件传递到子组件的技巧. 通常用来获取DOM节点或者React元素实例的工具.在React中Refs提供了一种方式,允许用户访问dom节点或者在render方法中创建的React

  • React组件refs的使用详解

    ref顾名思义我们知道,其实它就可以被看座是一个组件的参考,也可以说是一个标识.作为组件的属性,其属性值可以是一个字符串也可以是一个函数. 其实,ref的使用不是必须的.即使是在其适用的场景中也不是非用不可的,因为使用ref实现的功能同样可以转化成其他的方法来实现.但是,既然ref有其适用的场景,那也就是说ref自有其优势.关于这一点和ref的适用场景,官方文档中是这样说的: 在从 render 方法中返回 UI 结构之后,你可能想冲出 React 虚拟 DOM 的限制,在 render 返回的

  • React三大属性之Refs的使用详解

    refs是React中用来取得某个JSX组件或者某个DOM中的一些状态值的时候,用来获取节点的方法.在React官方的解释中,它的适用范围如下: 管理焦点,文本选择或媒体播放. 触发强制动画. 集成第三方 DOM 库. React文档中再三强调,请不要过度使用refs,所以当我们可以用dom原生对象解决时,尽量不要使用refs 依照之前的写法,首先是给出类组件和函数组件中refs的写法 类组件 在类中,refs有三种方式,目前最常用的是回调的形式使用,分别进行演示 //直接定义refs,已废弃

  • React中的refs的使用教程

    ref是React中的一种属性,当render函数返回某个组件的实例时,可以给render中的某个虚拟DOM节点添加一个ref属性,如下面的代码所示: <body> <script type="text/jsx"> var App = React.createClass({ render: function() { return ( <div> <input type="text" placeholder="inp

  • 浅谈React Refs 使用场景及核心要点

    目录 什么是 Refs? 使用方式 Refs 核心要点 避免重复创建 ref 内容 ref.current 存储的内容修改是突变 ref 作为数据存储时内容的变化不会引起 re-render ref 的读写只能在 useEffect 或者回调函数中进行 跨组件传递ref 获取dom时需要借助 forwardRef 包裹组件 ref 绑定的dom在离屏或者未挂载时ref.current 值会被修改为null 最佳实践 dom 操作相关 用于在两次 render 之间传递数据 在使用 React 进

  • 浅谈React中组件逻辑复用的那些事儿

    基本每个开发者都需要考虑逻辑复用的问题,否则你的项目中将充斥着大量的重复代码.那么 React 是怎么复用组件逻辑的呢?本文将一一介绍 React 复用组件逻辑的几种方法,希望你读完之后能够有所收获.如果你对这些内容已经非常清楚,那么略过本文即可. 我已尽量对文中的代码和内容进行了校验,但是因为自身知识水平限制,难免有错误,欢迎在评论区指正. 1. Mixins Mixins 事实上是 React.createClass 的产物了.当然,如果你曾经在低版本的 react 中使用过 Mixins,

  • 浅谈react.js中实现tab吸顶效果的问题

    在react项目开发中有一个需求是,页面滚动到tab所在位置时,tab要固定在顶部. 实现的思路其实很简单,就是判断当滚动距离scrollTop大于tab距离页面顶部距离offsetTop时,将tab的position变为fixed. 在react中,我在state中设置一个navTop属性,切换这个属性的值为true或者false,然后tab标签使用classnames()这个方法来利用navTop的值添加类名fixed. 一开始我是这样写的: import cs from 'classnam

  • 浅谈React深度编程之受控组件与非受控组件

    受控组件与非受控组件在官网与国内网上的资料都不多,有些人觉得它可有可不有,也不在意.这恰恰显示React的威力,满足不同规模大小的工程需求.譬如你只是做ListView这样简单的数据显示,将数据拍出来,那么for循坏与 {} 就足够了,但后台系统存在大量报表,不同的表单联动,缺了受控组件真的不行. 受控组件与非受控组件是React处理表单的入口.从React的思路来讲,作者肯定让数据控制一切,或者简单的理解为,页面的生成与更新得忠实地执行JSX的指令. 但是表单元素有其特殊之处,用户可以通过键盘

  • 浅谈React Router关于history的那些事

    如果你想理解React Router,那么应该先理解history.更确切地说,是history这个为React Router提供核心功能的包.它能轻松地在客户端为项目添加基于location的导航,这种对于单页应用至关重要的功能. npm install --save history 存在三类history,分别时browser,hash,与 memory.history包提供每种history的创建方法. import { createBrowserHistory, createHashHi

  • 浅谈Redis在直播场景的实践方案

    背景信息 视频直播间作为直播系统对外的表现形式,是整个系统的核心之一.除了视频直播窗口外,直播间的在线用户.礼物.评论.点赞.排行榜等数据信息时效性高,互动性强,对系统时延有着非常高的要求,非常适合使用Redis缓存服务来处理. 本篇最佳实践将向您展示使用Redis版搭建视频直播间信息系统的示例.您将了解三类信息的构建方法: 实时排行类信息 计数类信息 时间线信息 实时排行类信息 实时排行类信息包含直播间在线用户列表.各种礼物的排行榜.弹幕消息(类似于按消息维度排序的消息排行榜)等,适合使用Re

  • 浅谈react useEffect闭包的坑

    问题代码 看一段因为useEffect导致的闭包问题代码 const btn = useRef(); const [v, setV] = useState(''); useEffect(() => { let clickHandle = () => { console.log('v:', v); } btn.current.addEventListener('click', clickHandle) return () => { btn.removeEventListener('clic

  • 浅谈React双向数据绑定原理

    目录 什么是双向数据绑定 实现双向数据绑定 数据影响视图 视图影响数据 如果已经学过Vue,并且深入了解过Vue的双向数据绑定的话,就会明白 Vue 2.0 双向数据绑定的核心其实是通过Object.defineProperty来实现数据劫持和监听. 在 Vue 3.0 中则通过 Proxy来实现对整体对象的监听,对 Vue2.0 的优化. 什么是双向数据绑定 数据模型和视图之间的双向绑定. 当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化:可以这样说用户在视图

  • 浅谈C++高并发场景下读多写少的优化方案

    目录 概述 分析 双缓冲 工程实现上需要攻克的难点 核心代码实现 简单说说golang中双缓冲的实现 相关文献 概述 一谈到高并发的优化方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也能很大的影响整体性能,本文从单模块下读多写少的场景出发,探讨其解决方案,以其更好的实现高并发.不同的业务场景,读和写的频率各有侧重,有两种常见的业务场景: 读多写少:典型场景如广告检索端.白名单更新维护.loadbalance

  • 浅谈React底层实现原理

    目录 1. props,state与render函数关系,数据和页面如何实现互相联动? 2. React中的虚拟DOM 常规思路 改良思路(仍使用DOM) React的思路 深入理解虚拟DOM 3. 虚拟DOM的diff算法 4. React中ref的使用 5. React中的生命周期函数 6. 生命周期函数的使用场景 1. props,state与render函数关系,数据和页面如何实现互相联动? 当组件的state或者props发生改变的时候,自己的render函数就会重新执行.注意:当父组

随机推荐