目录
场景一:对于类组件可以通过 ref 直接获取组件实例,实现父 <-> 子 双向通信。
场景二:函数组件 forwardRef + useImperativeHandle
【ref作用】:最熟悉的就是 用 Ref 获取真实 DOM 元素和获取类组件实例,除此之外的功能,ref 派生出一些其他的高级用法,能够解决一些特殊场景下的问题
React中提供两种方法创建ref对象:
第一种: 类组件中 React.createRef 创建一个 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> }
第二种:函数组件中 React.useRef 创建 Ref
export default function Index(){ const currentDom = React.useRef(null) React.useEffect(()=>{ console.log( currentDom.current ) // div },[]) return <div ref={ currentDom } >ref对象模式获取元素或组件</div> }
【什么是 ref 对象,所谓 ref 对象就是用
createRef
或者useRef
创建出来的对象】【问:在函数组件中为什么不能用 createRef ?】
答:类组件有一个实例 instance 能够维护像 ref 这种信息,但函数组件每次更新时所有的变量都会重新声明,此时 ref 就会随着函数组件执行被重置。
类组件获取 Ref 三种方式
① Ref属性是一个字符串。
/* 类组件 */ class Children extends Component{ render=()=><div>hello,world</div> } /* TODO: Ref属性是一个字符串 */ export default class Index extends React.Component{ componentDidMount(){ console.log(this.refs) } render=()=> <div> <div ref="currentDom" >字符串模式获取元素或组件</div> <Children ref="currentComInstance" /> </div> }
React 在底层逻辑,会判断类型,如果是 DOM 元素,会把真实 DOM 绑定在组件 this.refs (组件实例下的 refs )属性上,如果是类组件,会把子组件的实例绑定在 this.refs 上。(函数组件没有实例,不能被 Ref 标记)
② Ref 属性是一个函数。
class Children extends React.Component{ render=()=><div>hello,world</div> } /* TODO: Ref属性是一个函数 */ export default 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模式获取元素或组件</div> <Children ref={(node) => this.currentComponentInstance = node } /> </div> }
③ Ref属性是一个ref对象。
class Children extends React.Component{ render=()=><div>hello,world</div> } export default class Index extends React.Component{ currentDom = React.createRef(null) currentComponentInstance = React.createRef(null) componentDidMount(){ console.log(this.currentDom) console.log(this.currentComponentInstance) } render=()=> <div> <div ref={ this.currentDom } >Ref对象模式获取元素或组件</div> <Children ref={ this.currentComponentInstance } /> </div> }
高级用法1:forwardRef 转发 Ref
forwardRef 的初衷就是解决 ref 不能跨层级捕获和传递的问题。 forwardRef 接受了父级元素标记的 ref 信息,并把它转发下去,使得子组件可以通过 props 来接受到上一层级或者是更上层级的ref,大家可能对我这句话不是很理解,不过没关系,下面来从具体场景中分析 forwardRef 的真正用途。
场景一:跨层级获取
场景二: 合并转发ref
场景三:高阶组件转发
高级用法2:ref实现组件通信
场景一:对于类组件可以通过 ref 直接获取组件实例,实现父 <-> 子 双向通信。
如果有种场景不想通过父组件 render 改变 props 的方式,来触发子组件的更新,也就是子组件通过 state 单独管理数据层,针对这种情况父组件可以通过 ref 模式标记子组件实例,从而操纵子组件方法,这种情况通常发生在一些数据层托管的组件上,比如
<Form/>
表单,经典案例可以参考 antd 里面的 form 表单,暴露出对外的resetFields
,setFieldsValue
等接口,可以通过表单实例调用这些 API 。/* 子组件 */ class Son extends React.PureComponent{ state={ fatherMes:'', sonMes:'' } // 1. 子组件暴露方法 fatherSay 供父组件使用,子组件拿到父组件传来的值,然后存到state中的fatherMes身上。 fatherSay=(fatherMessage)=> this.setState({ fatherMes: fatherMessage}) /* 提供给父组件的API */ render(){ const { fatherMes, sonMes } = this.state return <div className="sonbox" > <div className="title" >子组件</div> // 2. 在这里应用父组件传来的值 <p>父组件对我说:{ fatherMes }</p> // 3. 把子组件输入框里的值存在state里定义的sonMes身上 <div className="label" >对父组件说</div> <input onChange={(e)=>this.setState({ sonMes:e.target.value })} className="input" /> // 4. 父组件提供给子组件 toF,toF中传的是方法,当子组件调用,就可以把子组件里的值sonMes传给父组件,进而执行setSonMes(sonMes) <button className="searchbtn" onClick={ ()=> this.props.toF(sonMes) } >to father</button> </div> } } /* 父组件 */ export default function Father(){ const [ sonMes , setSonMes ] = React.useState('') // setSonMes const sonInstance = React.useRef(null) /* 3. useRef 产生的 ref 对象。用来获取子组件实例 */ const [ fatherMes , setFatherMes ] = React.useState('') // 1.把父组件输入框里的值存起来 const toSon =()=> sonInstance.current.fatherSay(fatherMes) /* 2. 调用子组件实例方法,改变子组件state */ return <div className="box" > <div className="title" >父组件</div> // 5. 在这里应用子组件传来的值 <p>子组件对我说:{ sonMes }</p> // 1. 把父组件输入框里的值存起来, <div className="label" >对子组件说</div> <input onChange={ (e) => setFatherMes(e.target.value) } className="input" /> // 2. 点击发送给儿子时调用儿子暴露出的方法fatherSay来传递刚才存的值 <button className="searchbtn" onClick={toSon} >to son</button> // 3. 父组件用ref标记子组件 // 4. 父组件提供给子组件 toF,toF中传递的是方法 <Son ref={sonInstance} toF={setSonMes} /> </div> }
【流程分析】:
- 1 子组件暴露方法 fatherSay 供父组件使用,父组件通过调用方法可以设置子组件展示内容。
- 2 父组件提供给子组件 toFather,子组件调用,改变父组件展示内容,实现父 <-> 子 双向通信。
【效果】
场景二:函数组件 forwardRef + useImperativeHandle
对于函数组件,本身是没有实例的,但是 React Hooks 提供了,useImperativeHandle 一方面第一个参数接受父组件传递的 ref 对象,另一方面第二个参数是一个函数,函数返回值,作为 ref 对象获取的内容。一起看一下 useImperativeHandle 的基本使用。
useImperativeHandle 接受三个参数:
- 第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
- 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
- 第三个参数 deps :依赖项 deps,依赖项更改形成新的 ref 对象。
forwardRef + useImperativeHandle 可以完全让函数组件也能流畅的使用 Ref 通信。其原理图如下所示:
// 子组件 function Son (props,ref) { const inputRef = useRef(null) const [ inputValue , setInputValue ] = useState('') useImperativeHandle(ref,()=>{ const handleRefs = { onFocus(){ /* 声明方法用于聚焦input框 */ inputRef.current.focus() }, onChangeValue(value){ /* 声明方法用于改变input的值 */ setInputValue(value) } } return handleRefs },[]) return <div> <input placeholder="请输入内容" ref={inputRef} value={inputValue} /> </div> } const ForwarSon = forwardRef(Son) // 父组件 class Index extends React.Component{ cur = null handerClick(){ const { onFocus , onChangeValue } =this.cur onFocus() // 让子组件的输入框获取焦点 onChangeValue('let us learn React!') // 让子组件input } render(){ return <div style={{ marginTop:'50px' }} > <ForwarSon ref={cur => (this.cur = cur)} /> <button onClick={this.handerClick.bind(this)} >操控子组件</button> </div> } }
效果图
流程分析:
- 父组件用 ref 标记子组件,由于子组件 Son 是函数组件没有实例,所以用 forwardRef 转发 ref。
- 子组件 Son 用 useImperativeHandle 接收父组件 ref,将让 input 聚焦的方法 onFocus 和 改变 input 输入框的值的方法 onChangeValue 传递给 ref 。
- 父组件可以通过调用 ref 下的 onFocus 和 onChangeValue 控制子组件中 input 赋值和聚焦。