浅谈useRef的使用和渲染机制

前言

刚开始使用react时,由于对react的hook不太了解,导致在使用useState时,出现了闭包的问题,当时搜索解决方法时,发现了useRef这个hook可以很快的解决这个问题。这里用来记录下自己对useRef这个hook的理解。

useRef的渲染机制

先要了解react中useRef和useState的区别,useState是用来管理组件状态的,而useRef是用来管理组件引用的。useState会导致组件重新渲染,而useRef不会。对应上面说的useRef解决闭包问题,其实不是react设置该hook的初衷,useRef这个hook的初衷是用来解决DOM操作问题的。能够解决闭包问题也只是其副作用之一。对于useRef的渲染机制我们可以总结以下几个关键点:

  • useRef在组件首次渲染时创建一个对象 { current: initialValue }
  • 整个组件的生命周期,不会创建新对象,返回的都是首次创建对象的引用
  • 无论如何赋值,都不会导致组件重新渲染(React通过Object.is比较检测不到变化,因此不会触发渲染)

以下是基于useRef的渲染机制的代码示例,组件使用了antd的Button和Card组件,从代码运行中我们可以看出,点击更新Ref值按钮,组件没有重新渲染,但是点击更新State值按钮,组件会重新渲染。

ini 复制代码
import { Button, Card } from "antd";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";

const DemoRef = () => {
    const ref = useRef<any>(null)
    // 渲染次数
    const renderCountRef = useRef(1);
    const renderValueRef = useRef<any>(0);
    const [renderValue, setRenderValue] = useState<any>(0);

    useEffect(() => {
        renderCountRef.current = renderCountRef.current + 1;
        console.log(`🔄 组件第 ${renderCountRef.current + 1} 次渲染`);
    });

    return <div className="flex w-full">
        <Card title="useRef的渲染机制" className="ml-12px" hoverable={true}>
            <div className="mt-12px max-w-200px text-[#e74c3c] bg-[#fffacd] p-4 text-[18px] font-bold flex w-full flex-row w-400px">
                组件渲染此时:<span className="font-bold">{renderCountRef.current}</span>
            </div>
            <Button onClick={() => {
                renderValueRef.current = renderCountRef.current + 1;
            }} className="mt-12px">更新Ref值</Button>
            <div className="mt-12px font-bold mb-12px">当前Ref值:{renderValueRef.current}(点击虽然新增了,但是组件没有重新渲染,导致此处仍然时老的值)</div>
            <Button onClick={() => {
                setRenderValue((pre: number) => pre + 1);
            }}>更新State值</Button>
            <div className="mt-12px font-bold">当前State值:{renderValue}(点击会触发组件重新渲染,导致此处的值会更新)</div>
        </Card>

    </div>
}

useRef解决的问题

  • 解决闭包问题:useRef能够在闭包函数中访问到最新的状态或属性是因为.current属性的引用不会改变。实际编码中以下两个场景会产生闭包,计时器显示和事件函数监听,下面分享下useRef在这两种场景的应用

    • 计时器中使用最新的状态或属性
    ini 复制代码
    const [duration, setDuration] = useState(0);
    const durationRef = useRef(duration);
    useEffect(() => {
      const interval = setInterval(() => {
        durationRef.current = durationRef.current + 1;
        setDuration(durationRef.current);
      }, 1000);
      return () => clearInterval(interval);
    }, []);
    • 事件处理函数中使用最新的状态或属性,此处不再列举代码和说明,因为和计时器的场景类似。
  • react组件中DOM操作:useRef可以用来操作DOM元素,例如获取输入框的值、滚动到指定位置等。 const inputRef = useRef(null); const handleClick = () => { inputRef.current.focus(); };

  • 解决性能问题,方便避免重复创建ref的内容

useRef的好兄弟forwardRef

在日常的开发中,多层组件嵌套是常有的场景,例如父组件中嵌套子组件,子组件中又嵌套孙子组件等。在这种场景下,我们偶尔会需要在父组件中操作子组件的DOM。这时候,如果直接在子组件上使用useRef,会获取不到子组件的DOM并且控制还会报错,原因是react为了保证组件的封装性,默认情况下自定义的组件是不会暴漏其内部DOM节点的ref,具体错误大家可以自己试试。报错提示中会提示我们在子组件中需要使用forwardRef来转发ref。

  • 以下是基于forwardRef的代码示例,从代码运行中我们可以看出,点击设置子组件的年龄为18按钮,子组件的年龄会更新为18。
  • 使用方式:子组件使用forwardRef包裹,需要转发的方式使用useImperativeHandle
typescript 复制代码
import { Button, Card } from "antd";
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";

const LoginForm = forwardRef((props: any, ref: React.ForwardedRef<{ reset: (flag: boolean) => void; }>) => {
    const { name } = props;
    const [formData, setFormData] = useState({
        username: '',
        password: ''
    });
    useImperativeHandle(ref, () => ({
        // 重置表单内容
        reset: () => {
            setFormData({
                username: '',
                password: ''
            })
        },
    }));
    return <div className="mt-12px font-bold">
        <h2>{name}</h2>
        <div>
            <div className="mt-12px">
                用户名:<Input type="text" value={formData.username} onChange={(e) => {
                    setFormData({
                        ...formData,
                        username: e.target.value
                    })
                }} />
            </div>
            <div className="mt-12px">
                密码:<Input type="password" value={formData.password} onChange={(e) => {
                    setFormData({
                        ...formData,
                        password: e.target.value
                    })
                }} />
            </div>
        </div>
    </div>
})
//父组件
  <Card title="useRef和useForwardRef的组合" className="ml-12px" hoverable={true}>
            <Button onClick={() => {
                childRef.current?.reset();
            }} type="primary">重置</Button>
            <Divider></Divider>
            <LoginForm name="登录表单" ref={childRef} />
        </Card>
  • forwardsRef注意点
    • forwardsRef和useRef组合很方便操作子组件的DOM,但是我们尽量避免在父组件中直接操作子组件的DOM,因为这会破坏组件的封装性,导致代码难以维护。
    • forwardsRef和useRef组合通过useImperativeHandle转发的方法,我们可以在父组件中控制子组件的状态,但是如非必要此种也尽量少用,优先用 useState + props 传递状态(如父组件通过 isVisible props 控制子组件弹窗),而非用 ref 调用方法(ref 仅用于 "必须操作 DOM / 内部方法" 的场景)
相关推荐
王中阳Go2 小时前
面试被挂的第3次,面试官说:你懂的LLM框架,只够骗骗自己
面试·职场和发展
少卿2 小时前
React Compiler 完全指南:自动化性能优化的未来
前端·javascript
广州华水科技2 小时前
水库变形监测推荐:2025年单北斗GNSS变形监测系统TOP5,助力基础设施安全
前端
广州华水科技2 小时前
北斗GNSS变形监测一体机在基础设施安全中的应用与优势
前端
七淮2 小时前
umi4暗黑模式设置
前端
8***B2 小时前
前端路由权限控制,动态路由生成
前端
军军3603 小时前
从图片到点阵:用JavaScript重现复古数码点阵艺术图
前端·javascript
znhy@1233 小时前
Vue基础知识(一)
前端·javascript·vue.js
我的小月月3 小时前
🔥 手把手教你实现前端邮件预览功能
前端·vue.js