本篇从 React Refs 的使用场景、使用方式、注意事项,到 createRef 与 Hook useRef 的对比使用,最后以 React createRef 源码结束,剖析整个 React Refs,关于 React.forwardRef 会在下一篇文章深入探讨。
React 的核心思想是每次对于界面 state 的改动,都会重新渲染整个Virtual DOM,然后新老的两个 Virtual DOM 树进行 diff(协调算法),对比出变化的地方,然后通过 render 渲染到实际的UI界面,
使用 Refs 为我们提供了一种绕过状态更新和重新渲染时访问元素的方法;这在某些用例中很有用,但不应该作为 props 和 state 的替代方法。
在项目开发中,如果我们可以使用 声明式 或 提升 state 所在的组件层级(状态提升) 的方法来更新组件,最好不要使用 refs。
管理焦点(如文本选择)或处理表单数据: Refs 将管理文本框当前焦点选中,或文本框其它属性。
在大多数情况下,我们推荐使用受控组件来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的,每个状态更新都编写数据处理函数。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。要编写一个非受控组件,就需要使用 Refs 来从 DOM 节点中获取表单数据。
class NameForm extends React.Component { constructor(props) { super(props); this.input = React.createRef(); } handleSubmit = (e) => { console.log('A name was submitted: ' + this.input.current.value); e.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Name: <input type="text" ref={this.input} /> </label> <input type="submit" value="Submit" /> </form> ); } }
因为非受控组件将真实数据储存在 DOM 节点中,所以再使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
媒体播放:基于 React 的音乐或视频播放器可以利用 Refs 来管理其当前状态(播放/暂停),或管理播放进度等。这些更新不需要进行状态管理。
触发强制动画:如果要在元素上触发过强制动画时,可以使用 Refs 来执行此操作。
Refs 有 三种实现:
createRef 是 React v16.3 新增的API,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。
Refs 通常在 React 组件的构造函数中定义,或者作为函数组件顶层的变量定义,然后附加到 render() 函数中的元素。
export default class Hello extends React.Component { constructor(props) { super(props); // 创建 ref 存储 textRef DOM 元素 this.textRef = React.createRef(); } componentDidMount() { // 注意:通过 "current" 取得 DOM 节点 // 直接使用原生 API 使 text 输入框获得焦点 this.textRef.current.focus(); } render() { // 把 <input> ref 关联到构造器里创建的 textRef 上 return <input ref={this.textRef} /> } }
使用 React.createRef() 给组件创建了 Refs 对象。在上面的示例中,ref被命名 textRef,然后将其附加到 <input> DOM元素。
其中, textRef 的属性 current 指的是当前附加到 ref 的元素,并广泛用于访问和修改我们的附加元素。事实上,如果我们通过登录 myRef 控制台进一步扩展我们的示例,我们将看到该 current 属性确实是唯一可用的属性:
componentDidMount = () => { // myRef 仅仅有一个 current 属性 console.log(this.textRef); // myRef.current console.log(this.textRef.current); // component 渲染完成后,使 text 输入框获得焦点 this.textRef.current.focus(); }
在 componentDidMount 生命周期阶段,myRef.current 将按预期分配给我们的 <input> 元素; componentDidMount 通常是使用 refs 处理一些初始设置的安全位置。
我们不能在 componentWillMount 中更新 Refs,因为此时,组件还没渲染完成, Refs 还为 null。
不同于传递 createRef() 创建的 ref 属性,你会传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。
import React from 'react'; export default class Hello extends React.Component { constructor(props) { super(props); this.textRef = null; // 创建 ref 为 null } componentDidMount() { // 注意:这里没有使用 "current" // 直接使用原生 API 使 text 输入框获得焦点 this.textRef.focus(); } render() { // 把 <input> ref 关联到构造器里创建的 textRef 上 return <input ref={node => this.textRef = node} /> } }
React 将在组件挂载时将 DOM 元素传入ref 回调函数并调用,当卸载时传入 null 并调用它。在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 refs 一定是最新的。
像上例, ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。
这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。我们可以通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。
export default class Hello extends React.Component { constructor(props) { super(props); } componentDidMount() { // 通过 this.refs 调用 // 直接使用原生 API 使 text 输入框获得焦点 this.refs.textRef.focus(); } render() { // 把 <input> ref 关联到构造器里创建的 textRef 上 return <input ref='textRef' /> } }
尽管字符串 stringRef 使用更方便,但是它有一些缺点,因此严格模式使用 stringRef 会报警告。官方推荐采用回调 Refs。
当 ref 属性被用于一个普通的 HTML 元素时,React.createRef() 将接收底层 DOM 元素作为它的 current 属性以创建 ref ,我们可以通过 Refs 访问 DOM 元素属性。
当 ref 属性被用于一个自定义 class 组件时,ref 对象将接收该组件已挂载的实例作为它的 current,与 ref 用于 HTML 元素不同的是,我们能够通过 ref 访问该组件的props,state,方法以及它的整个原型 。
ref 是为了获取某个节点是实例,所以 你不能在函数式组件上使用 ref 属性,因为它们没有实例。
推荐使用 回调形式的 refs, stringRef 将会废弃(严格模式下使用会报警告),React.createRef() API 是 React v16.3 引入的更新。
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的值。返回的 ref 对象在组件的整个生命周期内保持不变。
function Hello() { const textRef = useRef(null) const onButtonClick = () => { // 注意:通过 "current" 取得 DOM 节点 textRef.current.focus(); }; return ( <> <input ref={textRef} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ) }
useRef() 比 ref 属性更有用。useRef() Hook 不仅可以用于 DOM refs, useRef() 创建的 ref 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。
function Timer() { const intervalRef = useRef(); useEffect(() => { const id = setInterval(() => { // ... }); intervalRef.current = id; return () => { clearInterval(intervalRef.current); }; }); // ... }
这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...}对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
// ReactCreateRef.js 文件 import type {RefObject} from 'shared/ReactTypes'; // an immutable object with a single mutable value export function createRef(): RefObject { const refObject = { current: null, }; if (__DEV__) { // 封闭对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。 Object.seal(refObject); } return refObject; }
其中 RefObject 为:
export type RefObject = {| current: any, |};
这就是的 createRef 源码,实现很简单,但具体的它如何使用,如何挂载,将在后面的 React 渲染中介绍,敬请期待。