自学内容网 自学内容网

使用ref操作DOM(React)

一、获取指向节点的ref

1、一般方式:

  • 引入useRef Hook函数,它用来创建可变的ref对象。一般用于访问DOM元素或在组件之间维持不变的值
import { useRef } from 'react';
  • 使用useRef来声明一个myRef
const myRef = useRef(null);
  • 最后,将myRef作为ref属性传递给想要获取的DOM节点的JSX标签
<div ref={myRef}>
  • useRef函数会返回一个对象,该对象有一个current属性。最初,myRef.current为null;创建完那个div节点时,会把对那个节点的引用放入myRef.current。但设置 myRef 的 current 值不会触发重新渲染。
// 你从事件处理器(如点击、输入等等)访问该DOM节点,并使用在其上面定义的任意浏览器 API,例如:
myRef.current.scrollIntoView();

2、ref回调方式:

背景:

当想要给一个有未知数量项的列表绑定ref时,上面的一般方法显然是行不通的,因为Hook 只能在组件的顶层被调用,不能在循环语句、条件语句或 map() 函数中调用 useRef。即下面的示例代码是不可取的:

<ul>
  {items.map((item) => {
    // 行不通!
    const ref = useRef(null);
    return <li ref={ref} />;
  })}

</ul>

解决方法:

方法1(有局限性):用一个 ref 引用其父元素,然后用 DOM 操作方法如 querySelectorAll 来寻找它的子节点。例:

import React, { useRef } from 'react';

function ParentComponent() {
  const parentRef = useRef(null);

  const handleButtonClick = () => {
    // 使用 querySelectorAll 获取所有子元素
    const children = parentRef.current.querySelectorAll('.child');
    
    // 遍历子元素并打印它们的文本内容
    children.forEach((child) => {
      console.log(child.textContent);
    });


  };

  return (
    <div>
      <div ref={parentRef} style={{ border: '1px solid black', padding: '10px' }}>
        <div className="child">Child 1</div>
        <div className="child">Child 2</div>
        <div className="child">Child 3</div>
      </div>
      <button onClick={handleButtonClick}>获取子元素并修改</button>
    </div>
  );
}

export default ParentComponent;

缺点:这种方法很脆弱,如果 DOM 结构发生变化,可能会失效或报错

方法2:将函数传递给 ref 属性,这称为 ref 回调。当需要设置 ref 时,React 将传入 DOM 节点来调用你的 ref 回调,并在需要清除它时传入 null。例:

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) => (
            <li
              key={cat}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat, node);
                } else {
                  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;
}

注:在这个例子中,itemsRef 保存的不是单个 DOM 节点,而是保存了包含列表项 ID 和 DOM 节点的 Map。(Ref 可以保存任何值!)

特点:

  • 清晰的引用管理:通过回调函数,可以灵活地管理 DOM 引用,并在组件的生命周期内做清理。
  • 动态更新:如果 ref 需要在不同条件下变化,回调函数可以轻松适应。
  • 直接访问 DOM:可以在需要时直接访问和操作 DOM 元素,而不需要依赖于额外的状态管理。

二、访问另一个组件的DOM节点

上面提到的组件都是浏览器元素的内置组件,React会将该ref的current属性设置为相应的DOM节点

但若将ref放在自定义的组件上,例<MyInput />,默认情况下会得到null

1、错误情况:

例:

import { useRef } from 'react';

function MyInput(props) {
  return <input {...props} />;
}

export default function MyForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

此时执行点击事件后,会控制台报错:Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

报错原因:默认情况下,React 不允许组件访问其他组件的 DOM 节点。甚至自己的子组件也不行!

2、正确情况:

背景:Refs 是一种脱围机制,应该谨慎使用。手动操作另一个组件的 DOM节点会使你的代码更加脆弱,所以想要暴露其DOM节点的组件必须选择该行为,一个组件可以指定将它的ref转发给一个子组件

使用API:forwardRef API

示例代码:

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

工作原理:

  1. MyInput 组件是使用 forwardRef 声明的。 这让从上面接收的 inputRef 作为第二个参数 ref 传入组件,第一个参数是 props 。
  2. MyInput 组件将自己接收到的 ref 传递给它内部的<input> 。
拓展:

上面那种方法是把DOM元素整个暴露出去了,所以能在父组件直接访问和操作该DOM元素,也就是说能够对子组件的DOM元素执行所有该元素支持的操作

若想限制暴露的功能,useImperativeHandle可以胜任:

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

三、React何时添加refs

更新分为两个阶段:

渲染阶段:React调用组件来确定页面上应该显示什么

提交阶段:React把变更应用于DOM

在 React 中,通常不建议在组件的渲染过程中访问 refs,因为在这个阶段,DOM 节点可能尚未存在或尚未更新。这会导致 ref.current 的值为 null,或者返回一个过时的 DOM 节点引用。

1、渲染期间的时间点

  • 在 React 的渲染周期中,组件的 render 方法执行时,DOM 节点尚未被创建。此时,如果你尝试访问 ref.current,它将返回 null,因为 DOM 元素还没有被挂载到 DOM 树中。

2、更新期间的时间点

  • 当组件更新时,React 会先更新虚拟 DOM,然后再实际更新真实 DOM。在这个过程中,如果你在渲染方法中访问 ref,它可能会返回未更新的 DOM 节点引用,导致你无法获取到最新的 DOM 状态。

推荐做法:

1、useEffect Hook:useEffect 会在组件渲染完成后执行,此时 DOM 节点已经被创建并挂载到 DOM 树上。因此,访问 ref.current 是安全的。

2、事件处理函数:在事件处理函数中访问 ref 是安全的,因为这些函数是在用户交互后执行的,此时 DOM 元素已经存在并可以安全访问。

四、注意: 

1、避免更改由React管理的DOM节点:更改DOM后,React就不知道该如何正确的管理它了,可能会导致崩溃现象

2、并不是完全不能修改,可以修改React没有理由更新的部分DOM。例:如果某些<div>在 JSX 中始终为空,React 将没有理由去变动其子列表。 因此,在那里手动增删元素是安全的。


原文地址:https://blog.csdn.net/m0_75074819/article/details/143526831

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!