Skip to main content

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 元素,就可以对其进行相关操作了。

refs

但这种方式存在一些缺点:

  1. 它需要 React 跟踪当前正在渲染的组件(因为其无法猜测 this),这使得 React 稍微变慢。
  2. 它不可组合,一个元素上无法绑定多个 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,还可以直接关联一个类组件,这样就可以直接调用该组件内部的方法。例如:

// 父组件
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>
)
}
}

虽然提供了这种方式,但这是一种反模式,相当于回到了 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,第二个回调函数返回一个对象,该对象是一个映射关系,映射关系中的键之后能够暴露给父组件使用。