使用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} />;
});
工作原理:
- MyInput 组件是使用 forwardRef 声明的。 这让从上面接收的 inputRef 作为第二个参数 ref 传入组件,第一个参数是 props 。
- 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 将没有理由去变动其子列表。 因此,在那里手动增删元素是安全的。