1、基本使用
React 提供两种方法创建 Ref 对象:
1.1、类组件React.createRef
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/src/ReactCreateRef.js
export function createRef() {
const refObject = {
current: null,
}
return refObject;
}
createRef 只做了一件事,就是创建了一个对象,对象上的 current 属性,用于保存通过 ref 获取的 DOM 元素,组件实例等。createRef 一般用于类组件创建 Ref 对象,可以将 Ref 对象绑定在类组件实例上,这样更方便后续操作 Ref。
注意:不要在函数组件中使用 createRef,否则会造成 Ref 对象内容丢失等情况。
1.2、函数组件useRef
export default 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 等信息就会被保存下来。
2、类组件获取 Ref 方式
2.1、Ref属性是一个函数
import React from "react";
class Children extends React.Component {
render = () => <div>hello,world</div>;
}
/* 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>
);
}
2.2、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>
}
3、高级用法
3.1、forwardRef 转发 Ref
forwardRef 的初衷就是解决 ref 不能跨层级捕获和传递的问题。
3.1.1 场景一:跨层级获取
// 孙组件
function Son (props){
const { grandRef } = props
return <div>
<div> i am alien </div>
<span ref={grandRef} >这个是想要获取元素</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>
}
}
forwardRef 把 ref 变成了可以通过 props 传递和转发。
3.1.2 场景二:合并转发ref
// 表单组件
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} />
}
- 通过 useRef 创建一个 ref 对象,通过 forwardRef 将当前 ref 对象传递给子组件。
- 向 Home 组件传递的 ref 对象上,绑定 form 孙组件实例,index 子组件实例,和 button DOM 元素。
3.1.3 场景三:高阶组件转发
如果通过高阶组件包裹一个原始类组件,就会产生一个问题,如果高阶组件 HOC 没有处理 ref ,那么由于高阶组件本身会返回一个新组件,所以当使用 HOC 包装后组件的时候,标记的 ref 会指向 HOC 返回的组件,而并不是 HOC 包裹的原始类组件,为了解决这个问题,forwardRef 可以对 HOC 做一层处理。
function HOC(Component){
class Wrap extends React.Component{
render(){
const { forwardedRef ,...otherprops } = this.props
return <Component ref={forwardedRef} {...otherprops} />
}
}
return React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> )
}
class Index extends React.Component{
render(){
return <div>hello,world</div>
}
}
const HocIndex = HOC(Index)
export default ()=>{
const node = useRef(null)
useEffect(()=>{
console.log(node.current) /* Index 组件实例 */
},[])
return <div><HocIndex ref={node} /></div>
}
3.2、ref实现组件通信
3.2.1 类组件 ref
如下场景,不想通过父组件 render 改变 props 的方式,来触发子组件的更新,也就是子组件通过 state 单独管理数据层,针对这种情况父组件可以通过 ref 模式标记子组件实例。
/* 子组件 */
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>
}
- 子组件暴露方法 fatherSay 供父组件使用,父组件通过调用方法可以设置子组件展示内容。
- 父组件提供给子组件 toFather,子组件调用,改变父组件展示内容,实现父 <-> 子 双向通信。
3.2.2 函数组件 forwardRef + useImperativeHandle
useImperativeHandle 接受三个参数:
- 第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
- 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
- 第三个参数 deps :依赖项 deps,依赖项更改形成新的 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 赋值和聚焦。
3.2.3 函数组件缓存数据
函数组件每一次 render ,函数上下文会重新执行。使用useRef,只要组件没有销毁,ref 对象就一直存在,这样做的好处:
- 第一个能够直接修改数据,不会造成函数组件冗余的更新作用。
- 第二个 useRef 保存数据,如果有 useEffect ,useMemo 引用 ref 对象中的数据,无须将 ref 对象添加成 dep 依赖项,因为 useRef 始终指向一个内存空间,所以这样一点好处是可以随时访问到变化后的值。
```jsx
const toLearn = [ { type: 1 , mes:’let us learn React’ } , { type:2,mes:’let us learn Vue3.0’ } ]
export default function Index({ id }){
const typeInfo = React.useRef(toLearn[0])
const changeType = (info)=>{
} useEffect(()=>{typeInfo.current = info /* typeInfo 的改变,不需要视图变化 */
},[ id ]) / 无须将 typeInfo 添加依赖项 / returnif(typeInfo.current.type===1){
/* ... */
}
}{
toLearn.map(item=> <button key={item.type} onClick={ changeType.bind(null,item) } >{ item.mes }</button> )
}
设计思路:
- 用一个 useRef 保存 type 的信息,type 改变不需要视图变化。
- 按钮切换直接改变 useRef 内容。
- useEffect 里面可以直接访问到改变后的 typeInfo 的内容,不需要添加依赖项。
<a name="gxnpO"></a>
# 4、ref 底层原理、源码
1、Ref,React 底层用两个方法处理:**commitDetachRef** 和 **commitAttachRef** ,Ref在commit阶段处理,因为DOM会在commit阶段才会拿到。
```jsx
// react-reconciler/src/ReactFiberCommitWork.js
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') { /* function获取方式。 */
currentRef(null);
} else { /* Ref对象获取方式 */
currentRef.current = null;
}
}
}
2、DOM 更新阶段,这个阶段会根据不同的 effect 标签,真实的操作 DOM 。
3、layout 阶段,在更新真实元素节点之后,此时需要更新 ref 。
// react-reconciler/src/ReactFiberCommitWork.js
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent: //元素节点 获取元素
instanceToUse = getPublicInstance(instance);
break;
default: // 类组件直接使用实例
instanceToUse = instance;
}
if (typeof ref === 'function') {
ref(instanceToUse); //* function 和 字符串获取方式。 */
} else {
ref.current = instanceToUse; /* function 和 字符串获取方式。 */
}
}
}
这一阶段,主要判断 ref 获取的是组件还是 DOM 元素标签,如果 DOM 元素,就会获取更新之后最新的 DOM 元素。 如果是函数式 ref={(node)=> this.node = node } 会执行 ref 函数,重置新的 ref 。
如果是 ref 对象方式。
node = React.createRef()
<div ref={ node } ></div>
会更新 ref 对象的 current 属性。达到更新 ref 对象的目的。
逻辑流程图:
5、问答
1、为什么更新之前要使用 commitDetachRef 重置一次?直接调用commitAttachRef 形成最新的 ref 不就可以了吗?
this.state.isShow && <div ref={()=>this.node = node} >元素节点</div>
在一次更新时候,恰好改变 state 中的 isShow ,那么如果不刻意去置空 ref ,接下来 div 会被真正的移除,而 this.node 依然指向原来的 dom 元素节点。