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.jsexport 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 } = propsreturn <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 = nullcomponentDidMount(){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.propsforwardRef.current={form:this.form, // 给form组件实例 ,绑定给 ref form属性index:this, // 给index组件实例 ,绑定给 ref index属性button:this.button, // 给button dom 元素,绑定给 ref button属性}}form = nullbutton = nullrender(){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.propsreturn <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.statereturn <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 = nullhanderClick(){const { onFocus , onChangeValue } =this.curonFocus() // 让子组件的输入框获取焦点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.jsfunction 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.jsfunction 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 元素节点。
