React Ref 建立虚拟与真实Dom的链接

React Ref 建立虚拟与真实Dom的链接

使用 ref 引用值

当希望组件“记住”某些信息,但又不想让这些信息触发新的渲染时,可以使用 ref 来实现。使用时,可以通过从 React 导入 useRef Hook 来为组件添加一个 ref:

为组件创建 ref
1
2
3
4
5
6
7
8
import { useRef } from 'react';

const ref = useRef(0);

// 此 Hook 返回一个对象,其中保存传入的取值
{
current: 0
}

之后,就可以使用 ref.current 属性访问该 ref 的当前值。这个值在 React 中被有意设置为可变的,意味着可以对它进行读写操作。像 state 一样,可以让它指向任何东西:字符串、对象,甚至是函数。但与 state 不同的是,ref 是一个普通的 JavaScript 对象,并且在 ref 值更新时,组件不会被重新渲染。因此,如果在更新取值后,若需要重新渲染组件才会显示新状态的场景,ref 并不是一个合适的选择,因为界面永远不会被刷新。

ref 和 state 的比较

在大多数情况下,React 官方建议使用 state,ref 并不常用。

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

何时使用 ref

通常,当组件需要“跳出” React 并与外部 API 通信时,会用到 ref —— 通常是不会影响组件外观的浏览器 API。以下是这些罕见情况中的几个:

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

如果组件需要存储一些值,但不影响渲染逻辑,可以选择 ref。

ref 的最佳实践

  • 将 ref 视为脱围机制。当使用外部系统或浏览器 API 时,ref 很有用。如果很大一部分应用程序逻辑和数据流都依赖于 ref,则可能需要重新考虑实现的方法。
  • 不要在渲染过程中读取或写入 ref.current 如果渲染过程中需要某些信息,请使用 state 代替。由于 React 不知道 ref.current 何时发生变化,即使在渲染时读取它也会使组件的行为难以预测。(唯一的例外是像 if (!ref.current) ref.current = new Thing() 这样的代码,它只在第一次渲染期间设置一次 ref。)

使用 ref 操作 Dom

ref 最常见的用法是访问 DOM 元素,例如,如果想以编程方式聚焦一个输入框,这种用法就会派上用场。当将 ref 传递给 JSX 中的 ref 属性时,比如 <div ref={myRef}>,React 会将相应的 DOM 元素放入 myRef.current 中。当元素从 DOM 中删除时,React 会将 myRef.current 更新为 null。下面总结如何使用 ref 来操作 Dom。

获取指向 Dom 节点的 ref

想要获取只想 Dom 节点的 ref 很简单,只需要将声明的 ref 作为 ref 属性值传递给想要获取的 DOM 节点的 HTML 标准 JSX 标签即可:

获取指向 div 节点的 ref
1
2
3
4
import { useRef } from 'react';
const myRef = useRef(null);

<div ref={myRef}>

当 React 为这个 <div> 创建好 DOM 节点时,React 会把对该节点的引用放入 myRef.current 中,然后就可以从事件处理器函数访问此 DOM 节点,并调用在其上定义的内置浏览器API了。例如:

获取指向 input 节点的 ref
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useRef } from 'react';

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

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

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

在上述示例中,在获取到 input 节点的 ref 后,通过 button 的事件处理函数 handleClick 调用 inputRef.current.focus(),实现对输入框的聚焦。

访问另一个组件的 DOM 节点

如果尝试将 ref 放在 自己定义组件 上,例如 <MyInput />,默认情况下会得到 null ,这是因为 React 不允许组件访问其他组件的 DOM 节点。但也不是就没有办法实现访问其他组件中的 Dom 节点,这时需要想要暴露其 DOM 节点的组件必须选择实现该行为。一个组件可以指定将它的 ref “转发”给一个子组件。例如 MyInput 使用 forwardRef API 进行 ref 转发:

ref 转发
1
2
3
const MyInput = forwardRef((props, ref) => {  
return <input {...props} ref={ref} />;
});

这时候:

  1. <MyInput ref={inputRef} /> 告诉 React 将对应的 DOM 节点放入 inputRef.current 中。但是,这取决于 MyInput 组件是否允许这种行为, 默认情况下是不允许的。
  2. MyInput 组件是使用 forwardRef 声明的。 这让从上面接收的 inputRef 作为第二个参数 ref 传入组件,第一个参数是 props 。
  3. MyInput 组件将自己接收到的 ref 传递给它内部的 <input>

完整的例子:

ref 转发完整示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { forwardRef, useRef } from 'react';

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

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

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

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

React 何时添加 refs

在 React 中,每次更新都分为 两个阶段

  • 在 渲染 阶段, React 调用组件来确定屏幕上应该显示什么。
  • 在 提交 阶段, React 把变更应用于 DOM。

通常,在第一次渲染期间,DOM 节点尚未创建,因此 ref.current 将为 null,在渲染更新的过程中,DOM 节点还没有更新。因此,React 在提交阶段设置 ref.current。在更新 DOM 之前,React 将受影响的 ref.current 值设置为 null。更新 DOM 后,React 立即将它们设置到相应的 DOM 节点。一般情况下,从事件处理器访问 refs 即可,但并非所有想要访问 Dom 节点的操作都可以有对应的事件处理函数来处理,这时可以借助 Effect 实现,在下一章中会进行总结。

另一方面,使用 ref 时,应避免更改由 React 管理的 DOM 节点 。对 React 管理的元素进行修改、添加子元素、从中删除子元素会导致不一致的视觉结果,甚至是崩溃。不过,可以安全地修改 React 没有理由更新的部分 DOM。 例如,如果某些 <div> 在 JSX 中始终为空,React 将没有理由去变动其子列表。 因此,在那里手动增删元素是安全的。

总结

使用 ref,可以在不触发重新渲染的情况下,记住某些变量值,最有用的点是可以使 ref 指向 Dom 节点,进而直接使用浏览器API,实现一些特殊的效果。

作者

Jeill

发布于

2024-03-20

更新于

2024-03-24

许可协议

评论