从零开始的react入门教程(十一),react ref 详解,三种写法与 ref 转发(传递)
壹 ❀ 引
在前面的文章中,我们介绍了react
的状态提升,随之引出了redux
以及context
,其实都说到底都是为了方便管理react
的状态,让数据交互与组件通信变得更为简单。我们知道,react
属于单向数据流,属性方法都像瀑布的水往下层组件流动,子组件获取父组件的属性也很简单,一个props
就能搞定。问题来了,那万一父组件需要获取子组件的属性方法?或者父组件需要直接操作子组件的DOM,这又该如何下手呢?这里就不得不提react中的refs
属性,本篇文章将围绕ref
用法(三种写法)以及ref
转发(传递)展开,文中的例子推荐复制后运行,了解下大致运转过程总是好的,那么本文开始。
贰 ❀ Refs基本用法
在react
的16.3版本引入了新的refs
创建模式,如果要使用refs
我们都推荐使用React.createRef
或者函数回调模式,下面的例子也会使用React.createRef
模式来介绍相关用法,当然对于另外两种模式(回调与字符串)后面也会介绍,以下例子还是基于create-react-app
项目,所以大家可以在文中提到的对应文件进行代码修改,然后本地运行项目即可。
贰 ❀ 壹 ref + DOM
react
提供了React.createRef
来创建一个ref
,然后将此ref
属性附加到你想操作的DOM以及想获取属性方法的子组件上,我们在index.js
文件中添加如下代码:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount(){
console.log(this.echoRef);
}
render() {
// 这里的ref就是必须这么写了,通过ref属性将this.echoRef与子组件关联
return <div ref={this.echoRef}>你好啊,echo。</div>
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
控制台执行npm start
运行项目,我们提前在componentDidMount
中输出了this.echoRef
,打开控制台,可以看到输出的是一个对象,对象有个current
属性,展开后发现current
指向的就是我们添加了this.echoRef
的div
元素。所以可以得知,通过this.refName.current
能访问到添加了ref
属性的DOM元素或者组件。
我们修改上述componentDidMount
中的输出代码为this.echoRef.current.innerHtml
,打开控制台:
那么问题来了,假设我创建了一个ref
,用在了多个元素上会怎么样呢?这里我们修改render
内部的代码为:
render() {
return (
<Fragment>
<div ref={this.echoRef}>你好啊,echo。</div>
<div ref={this.echoRef}>你好啊,时间跳跃。</div>
</Fragment>
)
}
保存后查看控制台,你会发现只有后面div
的生效了,也就说,ref
的绑定就像一个同名的变量赋值,它永远以最后关联的DOM为准,一个ref
只能关联一个,无法重复使用。假设需要关联多个,我们完全可以创建多个ref
,这一点大家可以自行尝试。
关于上述中,我们使用了Fragment
组件,在JavaScript中其实也有DocumentFragment
相关概念,意思就是最小文档片段。我们知道render
中只接受一个根元素作为最外层DOM,比如如下代码就会报错:
render() {
return (
<div ref={this.echoRef}>你好啊,echo。</div>
<div ref={this.echoRef}>你好啊,时间跳跃。</div>
)
}
传统做法是添加一个公有的父级div
将其包裹起来,比如:
render() {
return (
<div>
<div ref={this.echoRef}>你好啊,echo。</div>
<div ref={this.echoRef}>你好啊,时间跳跃。</div>
</div>
)
}
但这样会产生一层无意义的div
结构,虽然对于DOM优化来说影响微乎其微,但能少一层总是好的,我们打开控制台查看Fragment
包裹的html
结构,如图:
你会发现Fragment
并没有产生多余的DOM
结构,如果你了解过vue
或者微信小程序,react
的Fragment
对标vue
的template
与小程序的block
标签,题外话说到这里。
贰 ❀ 贰 ref + Class组件
前面的例子我们将ref
加在了DOM
上,现在我们试试加在一个子组件上,修改index.js
代码为:
import React, { Component, Fragment } from 'react';
import ReactDOM from 'react-dom';
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
// 这里调用了子组件的方法
this.echoRef.current.handleClick();
}
render() {
return (
<Children ref={this.echoRef} userName="echo" />
)
}
}
class Children extends Component {
constructor(props) {
super(props);
}
state = {
userName: '听风是风'
}
// 这个方法给父组件调用
handleClick = () => { console.log('我在调用子组件的方法。') }
render() {
return (
<div>你好,我是{this.props.userName}。</div>
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
打开控制台,你会发现当前this.echoRef.current
访问到的就是我们的Children
组件,而且你能看到Children
上声明的state
以及方法,我们通过this.echoRef.current.handleClick()
调用了子组件的方法,因此控制台输出了我在调用子组件的方法。
。
ref
不仅仅能获取子组件的属性,同样能像链式读取那样访问到自己的孙子组件,以及更下层的组件的属性,现在我们再嵌套一层组件Grandson
,如下:
import React, { Component, Fragment } from 'react';
import ReactDOM from 'react-dom';
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
// 这里调用了孙子组件的方法
this.echoRef.current.timeStepRef.current.handleClick();
}
render() {
return (
<Children ref={this.echoRef} userName="echo" />
)
}
}
class Children extends Component {
constructor(props) {
super(props);
this.timeStepRef = React.createRef();
}
state = {
userName: '听风是风'
}
// 这个方法给父组件调用
handleClick = () => { console.log('我在调用子组件的方法。') }
render() {
return (
<Grandson ref={this.timeStepRef} />
)
}
}
class Grandson extends Component {
constructor(props) {
super(props)
}
// 这个方法给祖父组件使用
handleClick = () => { console.log('我是给上上层组件使用的方法') }
render() {
return (
<div>你好,我是孙子组件。</div>
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
如上图的对象解构,我们在父级组件通过this.echoRef.current.timeStepRef.current.handleClick();
调用了孙子组件的方法,所以不管组件嵌套多少层,只有有定义ref
你就一定能向下访问到你想要的属性,当然,这种做法想想就知道非常不好!
贰 ❀ 叁 ref + 函数组件以及ref转发
默认情况来说,ref
不能添加在函数组件上,因为函数组件没有实例,如果你按照前面的做法代码会给出警告,比如下面这个例子:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
function Children () {
return <div>我是子组件</div>;
}
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
}
render() {
return (
<Children ref={this.echoRef} userName="echo" />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
Warning:Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
但是呢,我们是可以在函数组件内,为其它DOM
或者组件绑定ref
,比如这个例子:
import React, { createRef } from 'react';
import ReactDOM from 'react-dom';
function Children(props, ref) {
const inputRef =createRef();
const handleClick = ()=>{
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>点我让输入框聚焦</button>
</>
);
}
ReactDOM.render(
<Children />,
document.getElementById('root')
);
综合来解释,默认情况下,函数组件内部可以使用ref
去绑定其它的DOM或者组件,但是如果你要直接给一个子函数组件上添加ref
就会出现上面的警告,警告中也给出了解决方案,那就是使用React.forwardRef
。
PS:上述代码中出现的<></>
标签作用与Fragment
作用相同,这个做个补充。
我们先来看看如何能在函数组件上添加ref
属性而不报警告,看下面这个例子:
import React, { Component, forwardRef, useRef, useImperativeHandle, createRef } from 'react';
import ReactDOM from 'react-dom';
function Children(props, ref) {
// useRef是一个hook,你只用知道它可以创建ref
const inputRef = useRef();
// 你可以通过这种方式创建
// const inputRef = createRef()
const sayName = () => {
console.log(1);
}
useImperativeHandle(ref, () => ({
focus: () => {
// 这里操作的是input自带的focus方法
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
}
// 由于函数组件不能用ref,这里使用`forwardRef`包裹了一层
Children = forwardRef(Children);
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.log(this.echoRef);
this.echoRef.current.focus();
}
render() {
return (
<Children ref={this.echoRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
一下子出现了很多奇奇怪怪的方法,别急,我们来解释它们。现在你不用了解这些方法的原理,你只用大致知道它们做了什么,深入的学习应该在更后面,因为这里涉及到了hook
的知识。
react
在16.8版本新增了hook
特性,准确来说,hook
颠覆了我们之前Class
组件以及生命周期那一套。hook
中没有生命周期,所有的组件都是函数组件,很明显,函数组件内是没有this
的,那自然没办法从父级组件访问函数组件this(组件实例)
上的属性方法。但是假设现在我们就是要操作函数组件的DOM
,比如上面的例子。
我们来解释上面的代码,首先forwardRef
接受一个函数,此函数支持2个参数props与ref
,props
很好理解,就是上层传递下来的属性,而ref
自然也是上层传递下来的ref
。比较巧的是我们的Children
本身就是个函数,因此我们直接使用forwardRef
进行了包裹,可以理解为给组件升了个级。
而在Children
内部const inputRef = useRef()
这一句,也是使用hook
提供的API
创建一个ref
,他与createRef
的原理以及含义上有一定差异,不过这里我们就理解为创建了一个ref
。
紧接着inputRef
与函数组件内部的input
相关联,也就是说现在函数组件内是可以直接使用input
内置的属性方法。前面说了,函数组件自己内部还是可以使用ref
的,只是我们不能直接用ref
关联一个函数组件,但是前面我们通过forwardRef
给函数组件升了级。
紧接着,我们通过useImperativeHandle
将函数组件内部能访问到的input
上的属性方法,再次暴露给了父组件,useImperativeHandle
中的ref
其实就是上层传递的,这里就是通过此方法,将上层ref
与函数组件内部产生了关联。我们自己定义了一个focus
方法,而这个方法内部执行的却是input
的focus
方法,组件内部可以通过inputRef.current.focus
访问到input
的方法(希望你没绕晕。)
于是我们在父组件中通过this.echoRef.current.focus()
访问到了函数组件暴露给它的方法,而这个方法本质执行的其实是input
自带的focus
方法。
不知道你理解没有,这里我们通过forwardRef
帮父组件做了一次转发,父组件其实想访问的就是input
的方法,但是函数组件在中间隔了一层,父组件就没法直接拿到,而我们通过useImperativeHandle
帮父组件代劳了一次,成功达到了目的。
其实forwardRef
除了能让函数组件使用ref
外,还有另一种强大的作用就是转发ref
。
比如A>B>C
的组件结构,你在A中创建了一个ref
,你希望将这个ref
作为props
传递给B,然后在B中接受这个ref
再去去关联C,以达到在A中可以访问到C的属性。我们可以假想有一个hoc
的场景,父组件希望访问B组件,但是B组件被hoc
包裹了一层,也就是一个高阶组件。此时你的ref
假设绑定在了hoc
生成的B,那么ref
将访问hoc
组件而非B组件。那么怎么让父组件可以访问到这个B组件呢?我们可以借用forwardRef
:
import React, { Component, forwardRef } from 'react';
import ReactDOM from 'react-dom';
function hocComponent(Component) {
// 单纯包装了传入的组件,生成了一个新的组件,只是在生成中我们还用了forwardRef在外面包了一层
return forwardRef((props, ref) => {
return <Component {...props} ref={ref} />
})
}
class Parent extends Component {
constructor(props) {
super(props);
// 创建一个ref,这个ref随便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.log(this.echoRef);
this.echoRef.current.handleClick();
}
render() {
// 传入Children给高阶组件,得到了一个新组件
const Child = hocComponent(Children);
return (
<Child ref={this.echoRef} />
)
}
}
class Children extends Component {
constructor(props) {
super(props);
}
handleClick = () => {
console.log('给父级调用的方法')
}
render() {
return <>我是子组件啊</>;
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
这个例子就解释了hoc
的情况,这个例子相对上面参杂了hook
的例子来说应该好理解一点,这里就不多解释。至少到这里,我们解释了forwardRef
的两种作用,一是也可以给函数组件绑定ref
,第二点就是ref
转发,比如hoc
包裹,我们绕过绕过高阶组件,拿到高阶组件内部真正的组件属性。
另外!!!A>B>C
,假设B是函数组件,我们希望A的ref
绑定C从而访问C,其实还有一种做法,就是不要直接ref
绑定,而是把ref
作为props
传递下去后再绑定,这样不管B是不是函数组件,都能成功绑定到C,再来个例子:
import React, { Component, forwardRef } from 'react';
import ReactDOM from 'react-dom';
function Children(props) {
return (
// 子组件接受了这个ref,然后再通过ref进行绑定
<input ref={props.inputRef} />
);
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
}
handleClick = () => {
// 成功访问了子组件下的子组件
this.echoRef.current.focus();
}
render() {
return (
<>
<Children
inputRef={this.echoRef}//我们希望把这个ref作为props传递下去
/>
<button onClick={this.handleClick}>点我聚焦</button>
</>
);
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
因为给函数组件上添加ref
会警告,那么我们就不用ref
,而是把创建的ref
作为属性传下去,在子组件中接受后,再绑定给你要访问的DOM或者组件,这样不仅解决了函数组件绑定ref
的问题,还搞定了ref
转发的问题,一箭双雕。
叁 ❀ 回调模式与字符串模式
上面我们介绍了createRef
创建ref
的模式,接下来补充函数回调模式与字符串模式,因为用法介绍的比较多了,这里只是介绍写法。
叁 ❀ 壹 回调模式
在第二小节中,我们通过createRef()
模式介绍了ref
的基本用法与部分使用场景,其实react
还支持函数回调的形式来绑定ref
,这种模式下不需要借用createRef
创建一个ref
,而是直接将需要绑定的DOM或者组件传递到函数中进行关联,直接看个例子:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.inputRef);
}
setRef = (e) => {
this.inputRef = e;
}
// 此方法暴露给父组件
inputFocus = () => {
this.inputRef.focus();
}
render() {
return <input ref={this.setRef} />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.echoRef);
// 拿到子组件内定义的方法
this.echoRef.inputFocus();
}
// 定义绑定ref函数
setRef = (e) => {
this.echoRef = e;
}
render() {
return (
// 这里不再直接绑定ref,而是上述函数
<Children ref={this.setRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
在上述代码中我们并没使用API创建ref
,而是直接定义了一个函数,此函数接受的参数其实就是你所绑定此方法的DOM或者组件,比如在父组件中,我们通过ref={this.setRef}
将Children
与父组件中的this.echoRef
进行了绑定。而在子组件中,我们同样通过此方法绑定了input
,最终我们在父组件中通过this.echoRef.inputFocus
间接调用了input
的方法。
在父子组件的componentDidMount
中我们添加了输出信息,打开控制台可以看到父组件成功访问到了子组件,而子组件成功访问到它所绑定的input
。
上述代码中,我们还是通过ref={callback}
的形式进行子组件绑定,另外callback
也能作为props
传递后,再在子组件中进行绑定,比如:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
constructor(props) {
super(props);
// 子组件接受传递的回调进行绑定
props.setRef(this);
}
componentDidMount() {
console.dir(this.inputRef);
}
setRef = (e) => {
this.inputRef = e;
}
// 此方法暴露给父组件
inputFocus = () => {
this.inputRef.focus();
}
render() {
return <input ref={this.setRef} />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.echoRef);
// 拿到子组件内定义的方法
this.echoRef.inputFocus();
}
// 定义绑定ref函数
setRef = (e) => {
this.echoRef = e;
}
render() {
return (
// 这里直接把回调传递下去了
<Children setRef={this.setRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
在子组件的constructor
中我们通过props.setRef(this)
这一句,拿到父组件传递的callback
然后绑定了当前实例,从而达到目的。还记得前面提到的A>B>C
场景吗,回调模式也能这么玩,再来个例子:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
constructor(props) {
super(props);
}
render() {
// 父级传递来的props,最后给了input用了
return <input ref={this.props.setRef} />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.echoRef);
// 拿到子组件内定义的方法
this.echoRef.focus();
}
// 定义绑定ref函数
setRef = (e) => {
this.echoRef = e;
}
render() {
return (
// 这里直接把回调传递下去了
<Children setRef={this.setRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
关于回调模式就说到这里,我们最后来说下字符串模式。
叁 ❀ 贰 字符串模式
由于字符串模式在react
官方文档中已明确废弃,未来可能会移除,所以这里只给一个例子,在日常开发中不推荐这种做法进行ref
绑定:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
sayName=()=>{}
render() {
return <input />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.refs);
// 拿到子组件内定义的方法
// this.echoRef.focus();
}
render() {
return (
// 这里直接把回调传递下去了
<>
<Children ref='echoRef' />
<input ref='timeRef' />
</>
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
在上述代码中,我们通过ref='refName'
的形式为DOM
或者组件绑定ref
,同样,我们可以通过this.refs.refName
拿到对应绑定的组件,其实看到refs
这个负数,你就应该可以猜到它能访问当前组件实例中的所有ref
,上面的例子也验证了这一点,关于字符串形式就聊这么多。
肆 ❀ 总
OK,让我们回顾本文所提到的知识点,介绍了三种ref方式,React.createRef
,函数回调以及字符串模式,当然我们不推荐字符串,因为它已被官方废弃。
函数组件不支持为其添加ref
属性,因为函数组件没有实例,但我们提到可以通过forwardRef
对于函数组件进行包裹,毕竟对于hook
而言,组件都是函数组件,react
也是提供了此做法来解决ref
获取的问题。
我们介绍了A>B>C
的组件嵌套场景,如何将A中定义的ref
转发给C绑定了,除了forwardRef
做法外,我们知道ref
也能定义好后,作为props
向下传递。
另外,我们在通过ref
获取子组件属性时,比如获取一个函数,请注意函数的写法,比如这个例子中,我们能拿到sayName
,但拿不到sayAge
,这是因为后者本质上是绑定在原型上,无法通过这种方式直接访问,但是你可以通过原型找到它。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
constructor(props) {
super(props);
}
sayName = () => { }
sayAge() { }
render() {
// 父级传递来的props,最后给了input用了
return <input />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.echoRef);
}
// 定义绑定ref函数
setRef = (e) => {
this.echoRef = e;
}
render() {
return (
// 这里直接把回调传递下去了
<Children ref={this.setRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
要介绍的大概就这么多了,那么本文结束。