1、基本使用

React 提供两种方法创建 Ref 对象:

1.1、类组件React.createRef

  1. class Index extends React.Component{
  2. constructor(props){
  3. super(props)
  4. this.currentDom = React.createRef(null)
  5. }
  6. componentDidMount(){
  7. console.log(this.currentDom)
  8. }
  9. render= () => <div ref={ this.currentDom } >ref对象模式获取元素或组件</div>
  10. }

底层原理:

  1. // react/src/ReactCreateRef.js
  2. export function createRef() {
  3. const refObject = {
  4. current: null,
  5. }
  6. return refObject;
  7. }

createRef 只做了一件事,就是创建了一个对象,对象上的 current 属性,用于保存通过 ref 获取的 DOM 元素,组件实例等。createRef 一般用于类组件创建 Ref 对象,可以将 Ref 对象绑定在类组件实例上,这样更方便后续操作 Ref。

注意:不要在函数组件中使用 createRef,否则会造成 Ref 对象内容丢失等情况。

1.2、函数组件useRef

  1. export default function Index(){
  2. const currentDom = React.useRef(null)
  3. React.useEffect(()=>{
  4. console.log( currentDom.current ) // div
  5. },[])
  6. return <div ref={ currentDom } >ref对象模式获取元素或组件</div>
  7. }

底层原理:

useRef 和 createRef的 ref 保存位置不相同,类组件实例 instance 能够维护 ref 信息,但是函数组件每次更新都会重新初始化,所有变量重新声明,所以 useRef 不能像 createRef 把 ref 对象直接暴露出去,如果这样每一次函数组件执行就会重新声明 Ref,此时 ref 就会随着函数组件执行被重置,这就解释了在函数组件中为什么不能用 createRef 的原因。

为了解决这个问题,hooks 和函数组件对应的 fiber 对象建立起关联,将 useRef 产生的 ref 对象挂到函数组件对应的 fiber 上,函数组件每次执行,只要组件不被销毁,函数组件对应的 fiber 对象一直存在,所以 ref 等信息就会被保存下来。

2、类组件获取 Ref 方式

2.1、Ref属性是一个函数

  1. import React from "react";
  2. class Children extends React.Component {
  3. render = () => <div>hello,world</div>;
  4. }
  5. /* Ref属性是一个函数 */
  6. export default class Index extends React.Component {
  7. currentDom = null;
  8. currentComponentInstance = null;
  9. componentDidMount() {
  10. console.log(this.currentDom);
  11. console.log(this.currentComponentInstance);
  12. }
  13. render = () => (
  14. <div>
  15. <div ref={(node) => (this.currentDom = node)}>Ref模式获取元素或组件</div>
  16. <Children ref={(node) => (this.currentComponentInstance = node)} />
  17. </div>
  18. );
  19. }

image.png

2.2、Ref属性是一个ref对象

  1. class Children extends React.Component{
  2. render=()=><div>hello,world</div>
  3. }
  4. export default class Index extends React.Component{
  5. currentDom = React.createRef(null)
  6. currentComponentInstance = React.createRef(null)
  7. componentDidMount(){
  8. console.log(this.currentDom)
  9. console.log(this.currentComponentInstance)
  10. }
  11. render=()=> <div>
  12. <div ref={ this.currentDom } >Ref对象模式获取元素或组件</div>
  13. <Children ref={ this.currentComponentInstance } />
  14. </div>
  15. }

image.png

3、高级用法

3.1、forwardRef 转发 Ref

forwardRef 的初衷就是解决 ref 不能跨层级捕获和传递的问题。

3.1.1 场景一:跨层级获取

  1. // 孙组件
  2. function Son (props){
  3. const { grandRef } = props
  4. return <div>
  5. <div> i am alien </div>
  6. <span ref={grandRef} >这个是想要获取元素</span>
  7. </div>
  8. }
  9. // 父组件
  10. class Father extends React.Component{
  11. constructor(props){
  12. super(props)
  13. }
  14. render(){
  15. return <div>
  16. <Son grandRef={this.props.grandRef} />
  17. </div>
  18. }
  19. }
  20. const NewFather = React.forwardRef((props,ref)=> <Father grandRef={ref} {...props} />)
  21. // 爷组件
  22. class GrandFather extends React.Component{
  23. constructor(props){
  24. super(props)
  25. }
  26. node = null
  27. componentDidMount(){
  28. console.log(this.node) // span #text 这个是想要获取元素
  29. }
  30. render(){
  31. return <div>
  32. <NewFather ref={(node)=> this.node = node } />
  33. </div>
  34. }
  35. }

forwardRef 把 ref 变成了可以通过 props 传递和转发。

3.1.2 场景二:合并转发ref

  1. // 表单组件
  2. class Form extends React.Component{
  3. render(){
  4. return <div>{...}</div>
  5. }
  6. }
  7. // index 组件
  8. class Index extends React.Component{
  9. componentDidMount(){
  10. const { forwardRef } = this.props
  11. forwardRef.current={
  12. form:this.form, // 给form组件实例 ,绑定给 ref form属性
  13. index:this, // 给index组件实例 ,绑定给 ref index属性
  14. button:this.button, // 给button dom 元素,绑定给 ref button属性
  15. }
  16. }
  17. form = null
  18. button = null
  19. render(){
  20. return <div>
  21. <button ref={(button)=> this.button = button } >点击</button>
  22. <Form ref={(form) => this.form = form } />
  23. </div>
  24. }
  25. }
  26. const ForwardRefIndex = React.forwardRef(( props,ref )=><Index {...props} forwardRef={ref} />)
  27. // home 组件
  28. export default function Home(){
  29. const ref = useRef(null)
  30. useEffect(()=>{
  31. console.log(ref.current)
  32. },[])
  33. return <ForwardRefIndex ref={ref} />
  34. }

image.png

  • 通过 useRef 创建一个 ref 对象,通过 forwardRef 将当前 ref 对象传递给子组件。
  • 向 Home 组件传递的 ref 对象上,绑定 form 孙组件实例,index 子组件实例,和 button DOM 元素。

3.1.3 场景三:高阶组件转发

如果通过高阶组件包裹一个原始类组件,就会产生一个问题,如果高阶组件 HOC 没有处理 ref ,那么由于高阶组件本身会返回一个新组件,所以当使用 HOC 包装后组件的时候,标记的 ref 会指向 HOC 返回的组件,而并不是 HOC 包裹的原始类组件,为了解决这个问题,forwardRef 可以对 HOC 做一层处理。

  1. function HOC(Component){
  2. class Wrap extends React.Component{
  3. render(){
  4. const { forwardedRef ,...otherprops } = this.props
  5. return <Component ref={forwardedRef} {...otherprops} />
  6. }
  7. }
  8. return React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> )
  9. }
  10. class Index extends React.Component{
  11. render(){
  12. return <div>hello,world</div>
  13. }
  14. }
  15. const HocIndex = HOC(Index)
  16. export default ()=>{
  17. const node = useRef(null)
  18. useEffect(()=>{
  19. console.log(node.current) /* Index 组件实例 */
  20. },[])
  21. return <div><HocIndex ref={node} /></div>
  22. }

3.2、ref实现组件通信

3.2.1 类组件 ref

如下场景,不想通过父组件 render 改变 props 的方式,来触发子组件的更新,也就是子组件通过 state 单独管理数据层,针对这种情况父组件可以通过 ref 模式标记子组件实例。

  1. /* 子组件 */
  2. class Son extends React.PureComponent{
  3. state={
  4. fatherMes:'',
  5. sonMes:''
  6. }
  7. fatherSay=(fatherMes)=> this.setState({ fatherMes }) /* 提供给父组件的API */
  8. render(){
  9. const { fatherMes, sonMes } = this.state
  10. return <div className="sonbox" >
  11. <div className="title" >子组件</div>
  12. <p>父组件对我说:{ fatherMes }</p>
  13. <div className="label" >对父组件说</div> <input onChange={(e)=>this.setState({ sonMes:e.target.value })} className="input" />
  14. <button className="searchbtn" onClick={ ()=> this.props.toFather(sonMes) } >to father</button>
  15. </div>
  16. }
  17. }
  18. /* 父组件 */
  19. export default function Father(){
  20. const [ sonMes , setSonMes ] = React.useState('')
  21. const sonInstance = React.useRef(null) /* 用来获取子组件实例 */
  22. const [ fatherMes , setFatherMes ] = React.useState('')
  23. const toSon =()=> sonInstance.current.fatherSay(fatherMes) /* 调用子组件实例方法,改变子组件state */
  24. return <div className="box" >
  25. <div className="title" >父组件</div>
  26. <p>子组件对我说:{ sonMes }</p>
  27. <div className="label" >对子组件说</div> <input onChange={ (e) => setFatherMes(e.target.value) } className="input" />
  28. <button className="searchbtn" onClick={toSon} >to son</button>
  29. <Son ref={sonInstance} toFather={setSonMes} />
  30. </div>
  31. }
  • 子组件暴露方法 fatherSay 供父组件使用,父组件通过调用方法可以设置子组件展示内容。
  • 父组件提供给子组件 toFather,子组件调用,改变父组件展示内容,实现父 <-> 子 双向通信。

3.2.2 函数组件 forwardRef + useImperativeHandle

useImperativeHandle 接受三个参数:

  • 第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
  • 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
  • 第三个参数 deps :依赖项 deps,依赖项更改形成新的 ref 对象。
  1. // 子组件
  2. function Son (props,ref) {
  3. const inputRef = useRef(null)
  4. const [ inputValue , setInputValue ] = useState('')
  5. useImperativeHandle(ref,()=>{
  6. const handleRefs = {
  7. onFocus(){ /* 声明方法用于聚焦input框 */
  8. inputRef.current.focus()
  9. },
  10. onChangeValue(value){ /* 声明方法用于改变input的值 */
  11. setInputValue(value)
  12. }
  13. }
  14. return handleRefs
  15. },[])
  16. return <div>
  17. <input placeholder="请输入内容" ref={inputRef} value={inputValue} />
  18. </div>
  19. }
  20. const ForwarSon = forwardRef(Son)
  21. // 父组件
  22. class Index extends React.Component{
  23. cur = null
  24. handerClick(){
  25. const { onFocus , onChangeValue } =this.cur
  26. onFocus() // 让子组件的输入框获取焦点
  27. onChangeValue('let us learn React!') // 让子组件input
  28. }
  29. render(){
  30. return <div style={{ marginTop:'50px' }} >
  31. <ForwarSon ref={cur => (this.cur = cur)} />
  32. <button onClick={this.handerClick.bind(this)} >操控子组件</button>
  33. </div>
  34. }
  35. }。

流程分析:

  • 父组件用 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)=>{
    1. typeInfo.current = info /* typeInfo 的改变,不需要视图变化 */
    } useEffect(()=>{
    1. if(typeInfo.current.type===1){
    2. /* ... */
    3. }
    },[ id ]) / 无须将 typeInfo 添加依赖项 / return
    1. {
    2. toLearn.map(item=> <button key={item.type} onClick={ changeType.bind(null,item) } >{ item.mes }</button> )
    3. }
    }
  1. 设计思路:
  2. - 用一个 useRef 保存 type 的信息,type 改变不需要视图变化。
  3. - 按钮切换直接改变 useRef 内容。
  4. - useEffect 里面可以直接访问到改变后的 typeInfo 的内容,不需要添加依赖项。
  5. <a name="gxnpO"></a>
  6. # 4、ref 底层原理、源码
  7. 1RefReact 底层用两个方法处理:**commitDetachRef** **commitAttachRef** Refcommit阶段处理,因为DOM会在commit阶段才会拿到。
  8. ```jsx
  9. // react-reconciler/src/ReactFiberCommitWork.js
  10. function commitDetachRef(current: Fiber) {
  11. const currentRef = current.ref;
  12. if (currentRef !== null) {
  13. if (typeof currentRef === 'function') { /* function获取方式。 */
  14. currentRef(null);
  15. } else { /* Ref对象获取方式 */
  16. currentRef.current = null;
  17. }
  18. }
  19. }

2、DOM 更新阶段,这个阶段会根据不同的 effect 标签,真实的操作 DOM 。
3、layout 阶段,在更新真实元素节点之后,此时需要更新 ref 。

  1. // react-reconciler/src/ReactFiberCommitWork.js
  2. function commitAttachRef(finishedWork: Fiber) {
  3. const ref = finishedWork.ref;
  4. if (ref !== null) {
  5. const instance = finishedWork.stateNode;
  6. let instanceToUse;
  7. switch (finishedWork.tag) {
  8. case HostComponent: //元素节点 获取元素
  9. instanceToUse = getPublicInstance(instance);
  10. break;
  11. default: // 类组件直接使用实例
  12. instanceToUse = instance;
  13. }
  14. if (typeof ref === 'function') {
  15. ref(instanceToUse); //* function 和 字符串获取方式。 */
  16. } else {
  17. ref.current = instanceToUse; /* function 和 字符串获取方式。 */
  18. }
  19. }
  20. }

这一阶段,主要判断 ref 获取的是组件还是 DOM 元素标签,如果 DOM 元素,就会获取更新之后最新的 DOM 元素。 如果是函数式 ref={(node)=> this.node = node } 会执行 ref 函数,重置新的 ref 。

如果是 ref 对象方式。

  1. node = React.createRef()
  2. <div ref={ node } ></div>

会更新 ref 对象的 current 属性。达到更新 ref 对象的目的。

逻辑流程图:
image.png

5、问答

1、为什么更新之前要使用 commitDetachRef 重置一次?直接调用commitAttachRef 形成最新的 ref 不就可以了吗?

  1. this.state.isShow && <div ref={()=>this.node = node} >元素节点</div>

在一次更新时候,恰好改变 state 中的 isShow ,那么如果不刻意去置空 ref ,接下来 div 会被真正的移除,而 this.node 依然指向原来的 dom 元素节点。