React Hooks - useRef、createRef、forwardRef、useImperativeHandle

390 阅读3分钟

1、什么是 useRef

  • 使用:const refContainer = useRef(initialValue)

  • 特点

    • 和类组件的实例属性几乎完全一致
    • 返回一个可变的 ref 对象 ( current 属性 )
    • 跨越渲染周期存储数据
      • 组件的更新,不会更改 current 的值
      • 更新 current 值,不会导致组件的更新
      • 值的引用,随时可以立刻拿到更新后最新的值( 类似于组件的this )
  • 使用场景

    • 定义一个类似 global 的变量、定时器(获取到最新值)
    • 绑定 dom 元素、子组件

2、变量的"引用"

// 1、操作 - 使用 useState
//    1、点击第一个按钮 6 下
//    2、立刻点击 Alert 按钮
//    3、马上再点击第一个按钮4下(3s内完成点击)
//    4、等待控制台输出结果:6
import React, { useState } from 'react';
const LikeButton = () => {
  const [like, setLike] = useState(0)
  function handleAlertClick() {
    setTimeout(() => {
      // 形成闭包,所以弹出来的是当时触发函数时的like值
      console.log(`you clicked on ${like}`)
    }, 3000)
  }
  return (
    <>
      <button onClick={() => setLike(like + 1)}>{like}赞</button>
      <button onClick={handleAlertClick}>Alert</button>
    </>
  )
}
export default LikeButton
// 2、// 操作 - 使用 useRef
//    1、点击第一个按钮 6 下
//    2、立刻点击 Alert 按钮
//    3、马上再点击第一个按钮4下(3s内完成点击)
//    4、等待控制台输出结果:10

import React, { useRef } from "react";
const LikeButton = () => {
  
    let like = useRef(0); // 等同于声明 global 全局变量
    function handleAlertClick() {
        setTimeout(() => {
        	alert(`you clicked on ${like.current}`);
        }, 3000);
    }    
    return (
        <>
        	// 更新 like 值时并不会 re-render 
            <button
 onClick={() => {
 like.current = like.current + 1;
}}>
                {like.current}赞
            </button>
            
            // 显示更改后的值
            <button onClick={handleAlertClick}>Alert</button>
        </>
    );
};
export default LikeButton;

3、绑定DOM、子组件

  • 绑定DOM
  • 绑定子组件(函数组件)
    • forwardRef:转发 refs 到 DOM 组件
    • useImperativeHandle:向外暴露自定义方法
// 1、绑定DOM元素(自动获取焦点)
import React, { MutableRefObject, useRef } from 'react'
const TextInputWithFocusButton = () => {
   const inputEl = useRef(null) // 绑定DOM元素
   const handleFocus = () => {
       inputEl.current.focus()
   }
   return (
       <div>
           <input ref={inputEl} type="text" />
           <button onClick={handleFocus}>Focus the input</button>
       </div>
   )
}
export default TextInputWithFocusButton
// 2、绑定子组件,hooks组件中(必须使用forwardRef)
import React, {useState, useRef} from 'react'
import Son1 from './Son1'
const About = () => {
  const inputRef = useRef()
  const cliBtn = () => {
    inputRef.current.focus()
  }
  return (
    <div>
      {/* 将ref定义到父组件 */}
      <Son1 ref={inputRef}></Son1>
      <button onClick={cliBtn}>点击</button>
    </div>
  )
}
export default About

// 子组件
import React, {useRef, forwardRef} from 'react
// 1、子组件使用 forwardRef 包裹
// 2、此时子组件接受两个参数
//    参数1: props
//    参数2:父组件ref对象
// 3、缺点
//    ref 绑定的 input 元素的所有方法都直接暴露给App
//    App组件可以进行任意的操作
//    不能手动暴漏方法
const Son = (props, inputRef) => {
  const inputEl = useRef(null) // 绑定DOM元素
  console.log('props', props)
  return (
    <div>
      <input ref={inputRef} type="text" />
    </div>
  )
}
export default forwardRef(Son)
// 3、绑定子组件,hooks组件中,向外暴露自定义方法(必须使用forwardRef、并配合useImperativeHandle)
import React, {useState, useRef} from 'react'
import Son1 from './Son1'
import Son2 from './Son2'
const About = () => {
  const inputRef = useRef()
  const cliBtn = () => {
    inputRef.current.focus()
  }
  return (
    <div>
      {/* 将ref定义到父组件 */}
      {/* <Son1 ref={inputRef}></Son1> */}
      <Son2 ref={inputRef}></Son2>
      <button onClick={cliBtn}>点击</button>
    </div>
  )
}
export default About

// 子组件
import React, {useRef, forwardRef, useImperativeHandle} from 'react'
inteface IRef {
  focus: () => void
}
// 1、子组件使用 forwardRef 包裹
// 2、此时子组件接受两个参数
//    参数1: props
//    参数2:父组件ref对象
// 3、向外暴露自定义方法
const Son2 = (props, ref: Ref<IRef | undefined>) => {
  const inputRef = useRef(null) // 绑定DOM元素
  console.log('props', props)
  // 向外暴露自定义方法
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus()
    }
  }))
  
  return (
    <div>
      <input ref={inputRef} type="text" />
    </div>
  )
}
export default forwardRef(Son2)

4、useRef 与 createRef 的区别( 组件3个阶段)

  • 第一:useRef 与 createRef 没有差别
  • 第二:
    • createRef 每次render都会返回个新的引用
    • useRef 不会 随着组件的更新而重新创建
  • 第三:两者都会销毁
import React, { useState, useRef, createRef } from 'react'

const RefDifference: React.FC = () => {
    let [renderIndex, setRenderIndex] = useState(1)
    
    let refFromUseRef = useRef<number>()
    let refFromCreateRef = createRef()
    
    // 首次为空,重新渲染后保留之前的值,不再重新赋值
    if (!refFromUseRef.current) {
        refFromUseRef.current = renderIndex
    }
    // 首次为空,重新渲染后还是为空,即每次赋新值
    if (!refFromCreateRef.current) {
        refFromCreateRef.current = renderIndex
    }
    
    // 任何data、props值的改变都会重新渲染组件
    return (
        <>
            <p>{renderIndex}</p>
            <p>{refFromUseRef.current}</p>
            <p>{refFromCreateRef.current}</p>
            
            <button onClick={() => setRenderIndex((prev) => prev + 1)}>
                Cause re-render
            </button>
        </>
    )
}
export default RefDifference

5、useRef 源码

// mount 阶段
function mountRef<T>(initialValue: T): {|current: T|} {
  // 获取 hook 对象
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  if (__DEV__) {
    Object.seal(ref);
  }
  
  hook.memoizedState = ref;
  return ref;
}


// update 阶段
function updateRef<T>(initialValue: T): {|current: T|} {
  const hook = updateWorkInProgressHook();
  return hook.memoizedState;
}