Ref
Ref
现代前端框架的一大特点是响应式,开发人员通常不需要再去手动操作 DOM,只需要关心和 DOM 元素绑定的响应式数据即可。
但在有些场景中,确实需要手动操作 DOM,如:
- 管理焦点,文本选择或媒体播放。
- 触发强制动画。
- 集成第三方 DOM 库。
Ref 提供了一种访问在 render 方法中创建的 DOM 节点或 React 元素的方式。
在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改子组件,需要使用新的 props 重新渲染它。然而,有一些场景需要在典型数据流之外以命令式方式修改子组件,要修改的子组件可以是 React 组件的实例,也可以是 DOM 元素。对于这些情况,Ref 为其提供了可能性。
refs
在早期的时候,React 中 Ref 的用法非常简单,给一个字符串类型的值,之后在方法中通过 this.refs.xxx 就能够访问:
import React, { Component } from 'react'
export default class App extends Component {
clickHandle = () => {
console.log(this);
console.log(this.refs.inputRef);
this.refs.inputRef.focus();
}
render() {
return (
<div>
<input type="text" ref="inputRef"/>
<button onClick={this.clickHandle}>聚焦</button>
</div>
)
}
}
如上示例,input 上面挂了一个 ref 属性,对应的值为 inputRef。查看组件实例,可以看到该组件实例中的 refs 面保存了该 input 的 DOM 元素,拿到了 DOM 元素,就可以对其进行相关操作了。

但这种方式存在一些缺点:
- 它需要 React 跟踪当前正在渲染的组件(因为其无法猜测
this),这使得 React 稍微变慢。 - 它不可组合,一个元素上无法绑定多个
ref,而回调形式的ref是可组合的。
class MyComponent extends Component {
renderRow = (index) => {
// Ref 将会绑定到 DataTable 组件,而非 MyComponent 组件。
return <input ref={'input-' + index} />;
// It worked.
return <input ref={input => this['input-' + index] = input} />;
}
render() {
return <DataTable data={this.props.data} renderRow={this.renderRow} />
}
}
目前,该 API 已经过时,可能会在未来的版本被移除,官方建议使用回调函数或 createRef API 的方式来代替。
Callback Ref
如果是 React 16.3 之前的版本,官方推荐使用回掉 Ref,即函数形式:
import React, { Component } from 'react';
import ChildCom1 from "./ChildCom1"
class App extends Component {
constructor() {
super();
this.inputRef = element => {
this.inputDOM = element;
};
this.comRef = element => {
this.comInstance = element;
};
}
clickHandle = () => {
this.inputDOM.focus();
this.comInstance.test();
}
render() {
return (
<div>
<input type="text" ref={this.inputRef} />
<ChildCom1 ref={this.comRef} />
<div>
<button onClick={this.clickHandle}>聚焦并且触发子组件方法</button>
</div>
</div>
)
}
}
这种方式保证了 this 的稳定,是早期版本推荐的方式。
createRef
createRef 是 React 上的一个静态方法,可以创建一个 Ref 对象,而不再是通过字符串的形式。
import React, { Component } from 'react'
class App extends Component {
constructor(props) {
super();
this.inputRef = React.createRef();
console.log(this.inputRef); // {current: null}
}
clickHandle = () => {
console.log(this.inputRef); // {current: input}
this.inputRef.current.focus();
}
render() {
return (
<div>
<input type="text" ref={this.inputRef}/>
<button onClick={this.clickHandle}>聚焦</button>
</div>
)
}
}
createRef 的本质就是是返回了一个 {current: null} 的对象:
function createRef() {
var refObject = {
current: null
}
{
Object.seal(refObject)
}
return refObject
}
如果要获取 DOM 元素,可以通过 this.inputRef.current 来获取。
除了在 JSX 中关联 Ref,还可以直接关联一个类组件,这样就可以直接调用该组件内部的方法。例如:
- App.jsx
- ChildCom1.jsx
// 父组件
import React, { Component } from 'react';
import ChildCom1 from "./ChildCom1"
export default class App extends Component {
constructor(props) {
super();
this.comRef = React.createRef();
}
clickHandle = () => {
console.log(this.comRef); // {current: ChildCom1}
this.comRef.current.test();
}
render() {
return (
<div>
{/* ref 关联子组件 */}
<ChildCom1 ref={this.comRef}/>
<button onClick={this.clickHandle}>触发子组件方法</button>
</div>
)
}
}
import React, { Component } from 'react'
export default class ChildCom1 extends Component {
test = () => {
console.log("child");
}
render() {
return (
<div>ChildCom1</div>
)
}
}
虽然提供了这种方式,但这是一种反模式,相当于回到了 jQuery 时代,因此尽量避免这么做。
默认情况下,不能在函数组件上使用 ref 属性,因为函数组件没有实例,但是在函数组件内部是可以使用 ref 的。
forwardRef
Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递给子组件,换句话说,将 ref 转发给子组件。
在高阶组件场景下,如果传递一个 ref,会发现 Ref 关联的是高阶组件中返回增强组件,而非原来的子组件,也就说 ref 属性不可透传。
要解决这个问题就会涉及到 Ref 转发,React 提供了一个 React.forwardRef API 专门用来做 Ref 转发给子组件。
React.forwardRef 接受一个渲染函数,该函数接收 props 和 ref 参数并返回增强组件:
import React from "react";
function withLog(Com) {
class WithLogCom extends React.Component {
render() {
// 通过 this.props 能够拿到传递下来的 ref 并与子组件进行关联
const {forwardedRef, ...rest} = this.props;
return <Com ref={forwardedRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
// 渲染函数会自动传入 ref,然后将 ref 继续往下传递
return <WithLogCom {...props} forwardedRef={ref} />;
});
}
export default withLog;
源码层面,forwardRef 的实现大致如下:
function forwardRef(render) {
// ...
var elementType = {
$$typeof: REACT_FORWARD_REF_TYPE,
render: render
}
// ...
return elementType
}
useRef
在函数组件中,React 提供了 useRef Hook 来解决 Ref 的问题。
import React from 'react';
function App() {
const [counter, setCounter] = React.useState(1);
const inputRef = React.useRef();
function clickHandle() {
// {current: input}
console.log("inputRef:", inputRef);
setCounter(counter + 1);
}
return (
<div>
<button onClick={clickHandle}>+1</button>
<div>{counter}</div>
<div>
<input type="text" ref={inputRef} />
</div>
</div>
);
}
export default App;
createRef 虽然也可以创建 Ref,但在函数组件中与 useRef 还是有所区别:
useRef是 hooks 的一种,一般用于函数组件,而createRef一般用于类组件。useRef创建的ref对象在组件的整个生命周期内都不会改变,但是createRef创建的ref对象,组件每更新一次,ref对象就会被重新创建。
useRef 还接受一个初始值,这在关联 DOM 元素时通常没什么用,但作为存储不需要变化的全局变量时则非常方便:
import { useState, useEffect, useRef } from 'react';
function App() {
let timer = useRef(null);
const [counter, setCounter] = useState(1);
useEffect(() => {
timer.current = setInterval(() => {
console.log('触发了');
}, 1000);
},[]);
const clearTimer = () => {
clearInterval(timer.current);
}
function clickHandle(){
console.log(timer);
setCounter(counter + 1);
}
return (
<>
<div>{counter}</div>
<button onClick={clickHandle}>+1</button>
<button onClick={clearTimer}>Stop</button>
</>
)
}
export default App;
useImperativeHandle
useImperativeHandle 也是一个 Hook,一般配合 React.forwardRef 使用。由于函数组件没有实例,无法挂载 Ref,当父组件传入 Ref 时,子组件通过 useImperativeHandle 可以自定义要暴露给父组件的实例值。
import React, { useRef, useImperativeHandle } from 'react';
function Child(props, ref) {
const childRef = useRef();
/*
父组件的ref:
{
current: {
click: fn
}
}
*/
useImperativeHandle(ref, () => {
return {
click: () => {
console.log(childRef.current);
}
}
});
function clickHandle() {
console.log("child test");
}
return (
<div onClick={clickHandle} ref={childRef}>
child
</div>
);
}
// 需要做 ref 转发
export default React.forwardRef(Child);
useImperativeHandle 的第一个参数是父组件传入的 ref,第二个回调函数返回一个对象,该对象是一个映射关系,映射关系中的键之后能够暴露给父组件使用。