Why & When Refs
在典型的 React
数据流中,DOM
元素的修改都是通过 M-V
的形式重新渲染。M
主要是组件的本地 state
,以及父组件传递给子组件的 props
。
但是,在某些情况下,你需要在典型数据流之外,手动使用/操作 DOM
:
- 管理焦点,媒体播放,文本选择,触发强制动画等。
- 集成第三方
DOM
库。
这个时候,就不要用类似 document.getElementById()
这种原生方式了。React
提供了 Refs
这样一种方式,来访问 DOM
节点或在 render
方法中创建的 React
元素。
另外,refs
还可用于创建一个可变对象(mutable object),并且修改它不会触发组件更新。
但是,一定不能滥用 Refs
。避免使用 refs
来做任何可以通过声明式渲染(Declarative,DOM随状态(数据)更新而更新)来完成的事情。
Class 组件中的 Refs
在类组件中,目前有三种方式来创建和使用Refs:
String类Refs
: 官方表示会弃用的,过时的API
,还不支持函数组件。所以别再用了。回调Refs
:将 回调函数 作为ref
的一种方式。React.createRef
: 这是React@16.3
版本引入的 顶层API
。
String类refs
命名一个 string
作为元素的ref
属性,然后通过 this.refs[string]
的形式访问。我们来看一个媒体播放的示例:
import React from 'react';
class VideoPlay extends React.Component {
constructor(props) {
super(props);
}
componentDidMount () {
// 使用refs
this.refs.myVideo.play()
}
render() {
// 创建refs
return (
<video
ref='myVideo'
src="https://media.w3.org/2010/05/sintel/trailer.mp4"
playsinline=""
/>
);
}
}
过时 API
,了解一下即可。再次强调:不要再这样使用。
备注:本篇文章的示例验证,都是使用
React 16.12.0
版本。
回调Refs
你可以传递一个函数作为 ref
。这个函数中接受 React
组件实例或 DOM
元素作为参数,以便它们能在其他地方被存储和访问。React
在组件挂载时,会调用 ref
回调函数并传入 DOM
元素,当卸载时调用它并传入 null
。在 componentDidMount
或 componentDidUpdate
触发前,React
会保证 ref
一定是最新的。
上面的例子用 回调Refs
实现如下:
import React from 'react';
class VideoPlay extends React.Component {
constructor(props) {
super(props);
}
componentDidMount () {
// 使用refs
this.myVideo.play()
}
render() {
// 创建refs
return (
<video
ref={ (elem) => { this.myVideo = elem } }
src="https://media.w3.org/2010/05/sintel/trailer.mp4"
playsinline=""
/>
);
}
}
React.createRef
React 16.3
版本新增的这个顶层 API
,用于创建 ref
。组件挂载时,会把 DOM
元素或组件实例传给 ref
的 current
属性,并在组件卸载时置为 null
。ref
会在 componentDidMount
或 componentDidUpdate
生命周期钩子触发前更新。
ref
的值根据它所赋予的节点的类型有所不同:
- 当
ref
属性用于DOM
元素时,ref
接收底层DOM
元素作为其current
属性 :
import React from 'react';
class APP extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 textInput 的 DOM 元素
this.textInput = React.createRef();
}
focusTextInput = () => {
// 直接使用原生 API 使 text 输入框获得焦点
// 注意:我们通过 "current" 来访问 DOM 节点
this.textInput.current.focus();
}
render() {
// 告诉 React 我们想把 <input> ref 关联到
// 构造器里创建的 `textInput` 上
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
export default APP;
- 当
ref
属性用于自定义class
子组件时,ref
对象接收 组件的挂载实例 作为其current
属性。
import React from "react";
class APP extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 子组件的实例
this.textInput = React.createRef();
}
focusTextInput = () => {
// 注意:这里 “ref” 的 "current" 是子组件的实例
// 所以可以调用实例的 “setValue” 方法
this.textInput.current.setValue("hello");
};
render() {
// 告诉 React 我们想把 <input> ref 关联到
// 构造器里创建的 `textInput` 上
return (
<div>
<InputText ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
// 注意 仅在 InputText 组件声明为 class 的时候有用
class InputText extends React.Component {
constructor(props) {
super(props);
this.state = {
value: ""
};
}
// 演示用的实例方法,没现实意义
setValue = value => {
this.setState({ value });
};
render() {
return <input type="text" value={this.state.value} />;
}
}
export default APP;
如果子组件通过 import InputText from "./InputText"
的方式引入,InputText
就是子组件的实例。那这种 refs
作用于子组件的使用方式意义何在?可能用 refs
显得更语义化,规范化。比如使用了多个 InputText
的情况。
注意: 你不能在函数组件上像这样使用
ref
属性,因为函数组件没有实例。不过可以通过forwardRef转发refs使用,这个后面会讲到。
函数组件中的 Refs
首先,我们思考这样一个问题:类组件中三种 refs
的使用方式能否在函数组件中应用。我们来逐个分析一下。
首先,String类refs
。不能!官方明确表示不支持函数组件,也是过时 API
, Just forget about it。
其次,我们在函数组件中应用一下 回调refs
,比如写一个自动聚焦的 input
组件:
import React, { useState, useEffect } from "react";
function App() {
const [value, setValue] = useState("hellow hooks");
let inputRef = null;
console.log(inputRef);// 日志A
useEffect(() => {
console.log(inputRef); //日志B
inputRef.focus();
});
function inputHandler(val) {
setValue(val.target.value);
}
return (
<>
<input
type="text"
value={value}
ref={elem => {
inputRef = elem;
}}
onChange={inputHandler}
/>
</>
);
}
export default App;
上面是一个简单的
input
输入组件。通过
回调refs
创建一个ref
( 对应input
元素) 赋值给inputRef
变量。使用
useEffect
实现自动focus
功能。这里没有加依赖项[]
是想看看组件重新渲染以后,refs
的变化。发现功能一切正常。每次渲染,日志A 为
null
,日志B 为<input type="text" value="hellow hooks"></input>
(其中value
是当前最新值)。
以上,我们完全可以得出一个结论:至少在函数组件内部,回调refs
完全可用。但是从日志A
和日志B
看出,每次渲染都会通过回调refs
的方式重新创建一个 ref
赋值给 inputRef
变量。明显存在性能问题。
让我们再试试createRef
,还是上面的例子:
import React, { useState, useEffect } from "react";
function App() {
const [value, setValue] = useState("hellow hooks");
//创建 refs
const inputRef = React.createRef();
// 日志A
console.log(inputRef.current || null)
useEffect(() => {
// 日志B
console.log(inputRef.current);
inputRef.current.focus();
});
function inputHandler(val) {
setValue(val.target.value);
}
return (
<>
<input type="text" value={value} ref={inputRef} onChange={inputHandler} />
</>
);
}
export default App;
- 使用
createRef
创建一个inputRef
,直接赋予input
的ref
属性。比回调refs
优美一些。 - 功能正常。日志跟
回调refs
一样。
结论:和 回调refs
一样 react.createRef
同样可以在函数组件内部使用。当然也有同样的性能问题。
网上有看到文章说在函数组件内部,使用
react.createRef
永远获取不到refs
。这和我的验证不符,可能是由于React
版本差异。
这里有一点需要说明,上面的两个示例之所以有性能问题,是因为我们在函数组件中使用了 hooks
。而我们使用的两种 refs
并没有 hooks
特性。所以,我们需要更好的选择。
useRef hooks
更好的选择那就是 useRef hooks
,我们将要学习的新的 React Hooks
,也是今天的主角。
和createRef
相比,就是改用 useRef
这个 hooks API
来创建 ref
,使用上并无差别。
还是同样的示例:
import React, { useState, useEffect, useRef } from "react";
function App() {
const [value, setValue] = useState("hellow hooks");
//创建 refs 只有这里有修改
const inputRef = useRef();
// 日志A
console.log(inputRef.current)
useEffect(() => {
// 日志B
console.log(inputRef.current);
inputRef.current.focus();
});
function inputHandler(val) {
setValue(val.target.value);
}
return (
<>
<input type="text" value={value} ref={inputRef} onChange={inputHandler} />
</>
);
}
export default App;
- API:和
createRef
非常类似。useRef
也可以接受一个参数作为ref
的current
值。有个细微区别是createRef
的current
默认值是null
,而useRef
中默认是undefined
。 - 性能:重新渲染时,
inputRef
不会再被初始化。日志A
和日志B
都打印<input type="text" value="Hello Reac"></input>
,并且其中value
是当前最新值。 - 原理:和
useState
useEffect
一样,每个组件都有一个 “内存单元”,首次渲染的时候初始化,把Hooks
的数据按顺序存入各个内存单元格,重新渲染的时候再按顺序依次从单元格读取。
暴露 Ref 给父组件
在实际复杂业务场景中,你可能希望在父组件中引用子节点的 DOM
节点。也就是我们不光需要 Ref
,还要把 Ref
的控制权也交给父组件。
官方不建议如此,因为它会打破组件的封装,逻辑分散。所以我们要慎用,然后该用还得用。
当你决定如此,这里有三种办法:
- ref属性传递: 支持所有
React
版本,不支持函数子组件。不推荐 - props传递: 支持所有
React
版本,也支持函数子组件。 - 转发refs: 支持 React16.3及以上版本。也支持函数子组件。
父组件是类组件还是函数组件,只决定你是用
createRef
还是useRef
创建refs
。
ref属性传递
前面在介绍 createRef
的时候有提到:当 ref
属性用于自定义 class
子组件时,ref
对象接收组件的挂载实例 作为其 current
属性。
注意了,我们获取到的是子组件的实例,而不是具体的 DOM
元素。这个时候,你想操作子组件,还需要在子组件也创建 ref
, 并把相关操作封装成一个方法供父组件调用。这也是不推荐这种方式的原因。
比如在某些场景,我们想在父组件让子组件中的 input
输入框聚焦:
import React from "react";
class APP extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 子组件的实例
this.inputText = React.createRef();
}
// 按钮事件处理中,实现聚焦。
// 你也可以在 componentDidMount 中处理,实现自动聚焦。
focusInputText = () => {
// 注意:这里 “ref” 的 "current" 是子组件的实例
// 只能通过调用实例的 doFocus 方法达到控制目的
this.inputText.current.doFocus();
// 更不优雅的方式如下
this.inputText.current.inputRef.current.focus();
};
render() {
return (
<>
<InputText ref={this.inputText} />
<input
type="button"
value="Focus the text input"
onClick={this.focusInputText}
/>
</>
);
}
}
// 注意 仅在 InputText 组件声明为 class 的时候有用
class InputText extends React.Component {
constructor(props) {
super(props);
this.state = {
value: ""
};
// 子组件也需要创建一个ref
this.inputRef = React.createRef();
}
// 提供一个给父组件调用的方法
doFocus = () => {
this.inputRef.current.focus();
};
render() {
return <input type="text" value={this.state.value} ref={this.inputRef} />;
}
}
export default APP;
缺点很明显:
- 不支持函数子组件,很致命。
- 父子组件都需要创建
refs
,性能不佳。
props传递
顾名思义,既然 ref
属性作用于子组件,是返回整个组件实例。那么我们可以通过 props
传递 refs
。你只需要给它起一个独一无二的名字。
上面的示例修改后是这样的:
import React from "react";
class APP extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 子组件的实例
this.inputRef = React.createRef();
}
// 按钮事件处理中,实现聚焦。
focusInputText = () => {
// 注意:这里 “ref” 的 "current" 在子组件挂载的时候 已经指向 input 元素
this.inputRef.current.focus();
};
render() {
return (
<>
<InputText inputRef={this.inputRef} />
<input
type="button"
value="Focus the text input"
onClick={this.focusInputText}
/>
</>
);
}
}
// 注意 这里用函数组件也同样可以
class InputText extends React.Component {
constructor(props) {
super(props);
this.state = {
value: ""
};
// ref 通过参数获取
this.inputRef = props.inputRef;
}
render() {
return <input type="text" value={this.state.value} ref={this.inputRef} />;
}
}
export default APP;
复制上述代码到 codesandbox
验证,功能ok。发现没有,代码相对于 “ref属性”,精简优雅了许多。而且,也支持函数组件。
我们用 函数组件 + hooks
来实现上述示例,并且使用 useEffect
实现自动focus
:
import React, { useState, useEffect, useRef } from 'react';
const App = () => {
const [greeting, setGreeting] = useState('Hello React!');
const ref = useRef();
useEffect(() => ref.current.focus(), []);
const handleChange = event => setGreeting(event.target.value);
return (
<div>
<h1>{greeting}</h1>
<Input value={greeting} handleChange={handleChange} inputRef={ref} />
</div>
);
};
const Input = ({ value, handleChange, inputRef }) => (
<input
type="text"
value={value}
onChange={handleChange}
ref={inputRef}
/>
);
export default App;
转发refs - forwardRef
Ref转发
是通过React
提供的一个顶层API
-forwardRef
来实现。forwardRef
通过包装子组件的形式,允许子组件能接收ref
作为第二个参数,并将其向下传递(换句话说,“转发” 它)给子组件。前面提到过,将
ref
作为子组件的JSX
属性,是没法把ref
传递下去。函数组件不支持,类组件也只能获取到子组件的实例。现在通过forwardRef
包装即可实现。请看示例:
import React, {
useState,
useEffect,
useRef,
forwardRef,
} from 'react';
const App = () => {
const [greeting, setGreeting] = useState('Hello React!');
const handleChange = event => setGreeting(event.target.value);
const ref = useRef();
useEffect(() => ref.current.focus(), []);
return (
<div>
<h1>{greeting}</h1>
<Input value={greeting} handleChange={handleChange} ref={ref} />
</div>
);
};
const Input = forwardRef(({ value, handleChange }, ref) => (
<input
type="text"
value={value}
onChange={handleChange}
ref={ref}
/>
));
export default App;
以下是对上述示例发生情况的逐步解释:
- 我们通过调用
React.useRef
创建了一个ref
并将其赋值给ref
变量。 - 我们通过指定
ref
为JSX
属性,将其向下传递给<Input ref={ref}>
。 React
传递ref
给forwardRef
内函数(props, ref) => ...
,作为其第二个参数。- 我们向下转发该
ref
参数到<input ref={ref}>
,将其指定为JSX 属性
。 - 当
ref
挂载完成,ref.current
将指向<input/>
DOM
节点。
forwardRef + useImperativeHandle
我们通过 forwardRef 进行 refs转发,并配合 useImperativeHandle hooks,可以将函数子组件的指定元素和方法暴露给父组件使用。这在很多稍复杂的业务场景非常有用。
API 可抽象为: useImperativeHandle(refParam, arrowFunction, [depsArr])
refParam
: 必须。通过forwardRef
转发的父组件传递的ref
,也就是forwardRef
里函数的第二个参数。arrowFunction
: 必须。回调函数,该函数返回的对象将暴露给父组件访问。depsArr
: 非必须。一个依赖项数组。当依赖项改变的时候会重新调用arrowFunction
。
我们改一下上面的示例,让父组件可以直接调用子组件的方法来聚焦子组件中的 input
。
import React, {
useState,
useEffect,
useRef,
forwardRef,
useImperativeHandle
} from 'react';
const App = () => {
const [greeting, setGreeting] = useState('Hello React!');
const handleChange = event => setGreeting(event.target.value);
const ref = useRef();
useEffect(() => ref.current.doFocus(), []);
return (
<div>
<h1>{greeting}</h1>
<Input value={greeting} handleChange={handleChange} ref={ref} />
</div>
);
};
const Input = forwardRef( ({ value, handleChange }, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({ doFocus }), []);
const doFocus = () => {
inputRef.current.focus();
}
return (
<input
type="text"
value={value}
onChange={handleChange}
ref={inputRef}
/>
)
});
export default App;
可变对象
前面提到,refs
不光可以用于引用和操作Dom
,还可以用于创建可变对象。
我们在函数组件中使用useRef hook
来创建一个看看:
import React, { useState, useRef } from "react";
function App() {
const valRef = useRef(0);// React.createRef don't work
let normalVal = 0;
const [, setChange] = useState();
return (
<div style={{ padding: "100px 200px" }}>
refValue: {valRef.current} |
normalValue: {normalVal}
<button
onClick={() => {
valRef.current += 80;
normalVal += 1;
setChange({});
}}
>
+
</button>
</div>
);
}
export default App;
- 可变对象的改变,不会触发组件的重新渲染。所以示例增加调用了
setChange({})
来触发组件重新渲染,来展示可变对象的变化。 - 点击
+
号,你会发现refValue
会不断递增,而normalValue
始终为0. - 这是一个有趣的功能,暂时还不知道会在哪些场景会用到它。了解一下即可。
最佳实践
所谓的最佳实践,也就是对一些规则的总结:
- 尽量在必要的时候使用
refs
,除了个别需要手动获取/操作Dom的场景:管理焦点,媒体播放,文本选择,触发强制动画,集成第三方 DOM 库等。 - 不要再使用
String类refs
,类似this.refs.XX
这种形式。 - 类组件中:
React16.3
及以上版本建议使用React.createRef
这个API
,低版本使用回调refs
。 - 函数组件中:为性能考虑,建议都使用
useRef
这个hook
。即便它无状态,无副作用。 - 对于
refs
在父子组件间传递的情况,慎用但该用还得用,然后首推refs转发
,其次props传递
。