基本概念和使用
在React中,ref是用来访问DOM元素或者类组件实例的方式。接下来学习下它的基本用法和一些高阶用法。
本文是《react进阶实践指南》学习笔记。
创建Ref对象
类组件获取ref对象
class Index extends React.Component{
constructor(props){
super(props)
this.currentDom = React.createRef(null)
}
componentDidMount(){
console.log(this.currentDom)
}
render= () => <div ref={ this.currentDom } >ref对象模式获取元素或组件</div>
}
createRef 只做了一件事,就是创建了一个对象,对象上的 current 属性,用于保存通过 ref 获取的 DOM 元素,组件实例等。 createRef 一般用于类组件创建 Ref 对象,可以将 Ref 对象绑定在类组件实例上,这样更方便后续操作 Ref
函数组件获取ref对象
function Index(){
const currentDom = React.useRef(null)
React.useEffect(()=>{
console.log( currentDom.current ) // div
},[])
return <div ref={ currentDom } >ref对象模式获取元素或组件</div>
}
useRef 底层逻辑是和 createRef 差不多,就是 ref 保存位置不相同,类组件有一个实例 instance 能够维护像 ref 这种信息,但是由于函数组件每次更新都是一次新的开始,所有变量重新声明,所以 useRef 不能像 createRef 把 ref 对象直接暴露出去,如果这样每一次函数组件执行就会重新声明 Ref,此时 ref 就会随着函数组件执行被重置,这就解释了在函数组件中为什么不能用 createRef 的原因。
为了解决这个问题,hooks 和函数组件对应的 fiber 对象建立起关联,将 useRef 产生的 ref 对象挂到函数组件对应的 fiber 上,函数组件每次执行,只要组件不被销毁,函数组件对应的 fiber 对象一直存在,所以 ref 等信息就会被保存下来。
Ref属性的内部处理
-
字符串引用
class Children extends React.Component{ render=()=><div>hello,world</div> } class Index extends React.Component{ componentDidMount(){ console.log(this.refs) } render=()=> <div> <div ref="currentDom" >字符串模式获取元素或组件asasasasasasasasas</div> <Children ref="currentComInstance" /> </div> }用一个字符串 ref 标记一个 DOM 元素,一个类组件(函数组件没有实例,不能被 Ref 标记)。React 在底层逻辑,会判断类型,如果是 DOM 元素,会把真实 DOM 绑定在组件 this.refs (组件实例下的 refs )属性上,如果是类组件,会把子组件的实例绑定在 this.refs 上。
只不过这种在react18中会抛出一个警告,
Component "div" contains the string ref "currentDom". Support for string refs will be removed in a future major release. We recommend using useRef() or createRef() instead.
这个警告意味着在React中使用了字符串引用(string refs),而在未来的主要版本中,对字符串引用的支持将被移除。建议改用useRef()或createRef()来代替字符串引用。这样可以更好地适应React未来的更新。
-
函数回调
class Children extends React.Component{ render=()=><div>hello,world</div> } /* TODO: Ref属性是一个函数 */ class Index extends React.Component{ currentDom = null currentComponentInstance = null componentDidMount(){ console.log(this.currentDom) console.log(this.currentComponentInstance) } render=()=> <div> <div ref={(node)=> this.currentDom = node } >Ref模式获取元素或组件22</div> <Children ref={(node) => this.currentComponentInstance = node } /> </div> }当用一个函数来标记 Ref 的时候,将作为 callback 形式,等到真实 DOM 创建阶段,执行 callback ,获取的 DOM 元素或组件实例,将以回调函数第一个参数形式传入,所以可以像上述代码片段中,用组件实例下的属性
currentDom和currentComponentInstance来接收真实 DOM 和组件实例。
高阶用法
forwardRef 转发 Ref
- 跨层级获取ref
forwardRef 的初衷就是解决 ref 不能跨层级捕获和传递的问题。 forwardRef 接受了父级元素标记的 ref 信息,并把它转发下去,使得子组件可以通过 props 来接受到上一层级或者是更上层级的ref。
// 孙组件
import React from 'react';
function Son (props){
const { grandRef } = props
return <div>
<div> i am dog </div>
<span ref={grandRef} >这个是想要获取元素111</span>
</div>
}
// 父组件
class Father extends React.Component{
constructor(props){
super(props)
}
render(){
return <div>
<Son grandRef={this.props.grandRef} />
</div>
}
}
const NewFather = React.forwardRef((props,ref)=> <Father grandRef={ref} {...props} />)
// 爷组件
class GrandFather extends React.Component{
constructor(props){
super(props)
}
node = null
componentDidMount(){
console.log(this.node) // span #text 这个是想要获取元素
}
render(){
return <div>
<NewFather ref={(node)=> this.node = node } />
</div>
}
}
export default GrandFather
forwardRef 把 ref 变成了可以通过 props 传递和转发。这个用法还是非常绕的,正常使用第三方的redux库存储下引用就行了,感觉像是炫技。
-
合并转发ref
import React, { useRef, useEffect } from 'react'; // 表单组件 class Form extends React.Component { render() { return <div>表单组件内容</div>; } } // index 组件 class Index extends React.Component { componentDidMount() { const { forwardRef } = this.props; forwardRef.current = { form: this.form, // 给form组件实例 ,绑定给 ref form属性 index: this, // 给index组件实例 ,绑定给 ref index属性 button: this.button, // 给button dom 元素,绑定给 ref button属性 }; } form = null; button = null; render() { return ( <div> <button ref={(button) => (this.button = button)}>点击</button> <Form ref={(form) => (this.form = form)} /> </div> ); } } const ForwardRefIndex = React.forwardRef((props, ref) => ( <Index {...props} forwardRef={ref} /> )); // home 组件 export default function Home() { const ref = useRef(null); useEffect(() => { console.log(ref.current); }, []); return <ForwardRefIndex ref={ref} />; }流程主要分为几个方面:
- 1 通过 useRef 创建一个 ref 对象,通过 forwardRef 将当前 ref 对象传递给子组件。
- 2 向 Home 组件传递的 ref 对象上,绑定 form 孙组件实例,index 子组件实例,和 button DOM 元素。
forwardRef让 ref 可以通过 props 传递,那么如果用 ref 对象标记的 ref ,那么 ref 对象就可以通过 props 的形式,提供给子孙组件消费,当然子孙组件也可以改变 ref 对象里面的属性,或者像如上代码中赋予新的属性,这种 forwardref + ref 模式一定程度上打破了 React 单向数据流动的原则。当然绑定在 ref 对象上的属性,不限于组件实例或者 DOM 元素,也可以是属性值或方法。 -
高阶组件转发
如果通过高阶组件包裹一个原始类组件,就会产生一个问题,如果高阶组件 HOC 没有处理 ref ,那么由于高阶组件本身会返回一个新组件.
import React, { Component } from 'react'; // 高阶组件 const withHOC = (WrappedComponent) => { return class extends Component { render() { return <WrappedComponent {...this.props} />; } }; }; // 原始类组件 class MyComponent extends Component { render() { return <input type="text" ref={this.props.inputRef} />; } } // 使用高阶组件包裹原始类组件 const MyEnhancedComponent = withHOC(MyComponent); // 父组件 class ParentComponent extends Component { constructor(props) { super(props); this.inputRef = React.createRef(); } componentDidMount() { this.inputRef.current.focus(); // 这里会报错,因为ref无法正确传递 } render() { return <MyEnhancedComponent inputRef={this.inputRef} />; } } export default ParentComponent;由于高阶组件没有正确处理ref,导致ref无法正确传递给原始组件MyComponent,从而在父组件ParentComponent中调用this.inputRef.current.focus()时会报错。这就是高阶组件未处理ref可能导致的问题。
所以当使用 HOC 包装后组件的时候,标记的 ref 会指向 HOC 返回的组件,而并不是 HOC 包裹的原始类组件,为了解决这个问题,forwardRef 可以对 HOC 做一层处理。
const withHOC = (WrappedComponent) => { class HOC extends Component { render() { return <WrappedComponent {...this.props} forwardedRef={this.props.forwardedRef} />; } } return React.forwardRef((props, ref) => { return <HOC {...props} ref={ref} />; }); }; class MyComponent extends Component { render() { return <input type="text" ref={this.props.forwardedRef} />; } } const MyEnhancedComponent = withHOC(MyComponent); class ParentComponent extends Component { constructor(props) { super(props); this.inputRef = React.createRef(); } componentDidMount() { this.inputRef.current.focus(); } render() { return <MyEnhancedComponent forwardedRef={this.inputRef} />; } }
这样代码就能正确的获取到ref应用,做到主动聚焦。
ref实现组件通信
- 类组件
不想通过父组件 render 改变 props 的方式,来触发子组件的更新,也就是子组件通过 state 单独管理数据层,针对这种情况父组件可以通过 ref 模式标记子组件实例,从而操纵子组件方法,这种情况通常发生在一些数据层托管的组件上。
import React from 'react';
class Son extends React.PureComponent{
state={
fatherMes:'',
sonMes:''
}
fatherSay=(fatherMes)=> this.setState({ fatherMes }) /* 提供给父组件的API */
render(){
const { fatherMes, sonMes } = this.state
return <div className="sonbox" >
<div className="title" >子组件</div>
<p>父组件对我说:{ fatherMes }</p>
<div className="label" >对父组件说</div> <input onChange={(e)=>this.setState({ sonMes:e.target.value })} className="input" />
<button className="searchbtn" onClick={ ()=> this.props.toFather(sonMes) } >to father</button>
</div>
}
}
/* 父组件 */
export default function Father(){
const [ sonMes , setSonMes ] = React.useState('')
const sonInstance = React.useRef(null) /* 用来获取子组件实例 */
const [ fatherMes , setFatherMes ] = React.useState('')
const toSon =()=> sonInstance.current.fatherSay(fatherMes) /* 调用子组件实例方法,改变子组件state */
return <div className="box" >
<div className="title" >父组件</div>
<p>子组件对我说:{ sonMes }</p>
<div className="label" >对子组件说</div> <input onChange={ (e) => setFatherMes(e.target.value) } className="input" />
<button className="searchbtn" onClick={toSon} >to son</button>
<Son ref={sonInstance} toFather={setSonMes} />
</div>
}
这个例子中,子组件向父组件传参,很常规了,通过属性调用父组件的方法this.props.toFather(sonMes);这里父组件向子组件传参,用的并不是常规的props传参,是通过React.useRef(null)获取子组件的实例,通过调用子组件上方法来更改向子组件传参。
-
函数组件
函数组件,本身是没有实例的,但是提供了useImperativeHandle是一个非常有用的钩子函数。它允许你在父组件中访问子组件的方法和属性。
useImperativeHandle 接受三个参数:
- 第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
- 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
- 第三个参数 deps :依赖项 deps,依赖项更改形成新的 ref 对象。
const ChildComponent = forwardRef((props, ref) => {
const [childProperty, setChildProperty] = React.useState(props.childProperty);
useImperativeHandle(ref, () => ({
childMethod(value) {
setChildProperty(value);
},
childProperty
}));
return <div>{childProperty}</div>;
});
// 父组件
const ParentComponent = () => {
const childRef = useRef(null);
const [ value , setValue ] = React.useState('asdasdasdasd')
// 调用子组件的方法
const handleClick = () => {
childRef.current.childMethod(value);
};
// 获取子组件的属性
const childProperty = childRef.current ? childRef.current.childProperty : null;
// 父组件的渲染逻辑
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>Call Child Method</button>
<input onChange={ (e) => setValue(e.target.value) } className="input" />
</div>
);
};
上面代码中,父组件通过childRef.current.childMethod(value);来调用子组件的方法。
函数组件缓存数据
函数组件每一次 render ,函数上下文会重新执行,那么有一种情况就是,在执行一些事件方法改变数据或者保存新数据的时候,有没有必要更新视图,有没有必要把数据放到 state 中。如果视图层更新不依赖想要改变的数据,那么 state 改变带来的更新效果就是多余的。这时候更新无疑是一种性能上的浪费。
这种情况下,useRef 就派上用场了,上面讲到过,useRef 可以创建出一个 ref 原始对象,只要组件没有销毁,ref 对象就一直存在,那么完全可以把一些不依赖于视图更新的数据储存到 ref 对象中,这也是比较常用的做法。