likes
comments
collection
share

05|深入理解useRef

作者站长头像
站长
· 阅读数 83

基本概念

在 FunctionComponent 中创建 ref 需要使用到useRef这个hook,函数签名如下

const refContainer = useRef(initalValue)

useRef 函数接收一个初始值,并返回一个可变的(mutable) ref 对象,也就是我们的变量refContainer。这个对象上面会存在一个属性——current,它的默认值就是initalValue。这里有个比较重要的点就是useRef 创建返回的对象是可变的,并且它会在组件的整个生命周期中都存在,这也就以为着无论你在何时何地访问这个对象,它的值永远都是最新的值。

useState 创建出来的值是不可变的(immutable),每一次 render 都是新的值,而 useRef 创建出来的对象始终都是最开始的那个对象,其属性 current可以被赋值。

被 state 的数据更新所折磨的你是不是心里乐开了花儿 🤪 🤪 🤪

05|深入理解useRef

能替代useState吗?

不过不要高兴的太早,我们来看下它能不能代替“烦人”的 useState

function App() {
  
  const count = useRef(0)
  const addCount = () => {
    count.current++
  }

  const [num, setNum] = useState(0)
  const addNum = () => {
    setNum(num + 1)
  }
  
  const logCount = () => {
    console.log(count.current)
  }
  
  return (
    <div>
      <div>
        <span>count:{count.current}</span>
        <span>num:{num}</span>
      </div>
      <button onClick={addCount}>count+1</button>
      <button onClick={logCount}>查看count</button>
      <button onClick={addNum}>更新num,重新render</button>
    </div>
  )
}

效果如下👇

05|深入理解useRef

可以看到,尽管它的数值的确被改变了,但是它无法让我们的dom更新,也许你会觉得很奇怪,为什么数据改变了但是没有触发 render 呢?

要知道,我们使用 useState 都需要使用它提供的 dispatch (useState返回一个数组,第一个值是数据,第二个值是用来修改数据的方法,也就是dispatch)去触发函数组件 render 的,所以我们这里的 useRef 返回的这个对象与下面这样创建的对象从本质上来讲没有什么区别。

const obj = { current: 0 }

useRef 并不能替代掉 useState,因为它只是一个普通的变量,无法在更新数据的同时让视图同步更新。

useRef的本质是引用

注意:下面所有写到“函数组件(fiber)”其实都应该是fiber,如果你不理解fiber,那么就当它是函数组件也没关系

我们来透过源码来看useRef,会发现 useRef 创建的对象会存放到对应的函数组件(fiber)上,源码如下👇

05|深入理解useRef

当我们首次执行useRef(initialValue)时最终会执行mountRef,这个函数干如下五件事:

  1. 接收initialValue
  2. mountWorkInProgressHook中创建一个hook对象,并将其挂载到当前这个函数组件(fiber对象)上
  3. 创建一个 ref 对象的变量,将initialValue赋值到current属性上
  4. 将这个 ref 变量挂载到hook上(即挂在到组件上)
  5. 返回 ref

其中第四步也就将这个数据与组件关联起来了,所以这个数据不会被丢失,并且你能在任何地方访问到最新的ref的值就是这个原因

当再次render执行useRef就更加简单了

05|深入理解useRef

从函数组件(fiber)上拿到当前这个 hook,并返回这个对象。

所以useRef利用的就是 js 中的引用,将对象存储在函数组件(fiber)上,使得在任何时间任何地点都能访问到最新的值。

ref 的应用

ref 主要有以下3种应用方式

  1. 数据存储
  2. 获取 dom 对象
  3. 组件通讯

数据存储

数据存储一般用于与dom不相关的数据,比如我们做计时器类带需求时,可以用来存储定时器指针👇

function App() {

  const timer = useRef(null)
  const [num, setNum] = useState(0)
  
  const start = () => {
    timer.current = setInterval(() => {
      // 设置新的状态值时记得这样写~
      setNum(num => num + 1)
    }, 1000)
  }

  const clear = () => {
    clearInterval(timer.current)
  }

  useEffect(() => {
    // 组件销毁记得清除定时器
    return () => {
      clear()
    }
  }, [])

  return (
    <div>
      <div>{num}</div>
      <div>
        <button onClick={start}>start</button>
        <button onClick={clear}>clear</button>
      </div>
    </div>
  )
}

获取dom对象

function App() {
  
  const domRef = useRef()
  
  const handler = () => {
    console.log(domRef.current)
  }
  
  return (
    <div ref={domRef}>
      <button onClick={handler}>查看domRef</button>
    </div>
  )
}

组件通讯

ref在react中也可以充当一种通讯方式,比如父组件获取子组件的dom、数据或方法。

  1. 只获取子组件的dom
function Parent() {

  const childRef = useRef<HTMLDivElement>(null)
  
  const handler = () => {
    // 父组件这里就能获取到子组件内的dom了
    console.log(childRef.current)
  }
  
  return (
    <div>
      <button onClick={handler}>获取子组件</button>
      <RefChild ref={childRef} />
    </div>
  )
}

// 子组件接收 ref 参数
const Child: React.ForwardRefRenderFunction<HTMLDivElement, any> = (props, ref) => {
  return (
    // 将ref参数传递到原生组件上
    <div ref={ref}>child</div>
  )
}

// 经过 forwardRef 转发 ref
const RefChild = forwardRef(Child)
  1. 父组件获取子组件内到数据或方法,需要子组件中配合useImperativeHandle暴露出对应的数据与方法
function Parent() {

  const childRef = useRef<RefType>({
    num: 1,
    add: () => {}
  })
  
  const handler = () => {
    console.log(childRef.current)
    // { num: 1, add: Function }
  }
  
  return (
    <div>
      <button onClick={handler}>获取子组件数据</button>
      <RefChild ref={childRef} />
    </div>
  )
}

const Child: React.ForwardRefRenderFunction<RefType, any> = (props, ref) => {

  const [num, setNum] = useState(1)
  const add = () => {
    setNum(num + 1)
  }
  
  useImperativeHandle(ref, () => {
    return {
      num,
      add
    }
  })
  
  return <div onClick={add}>{num}</div>
}

const RefChild = forwardRef(Child)

interface RefType {
  num: number,
  add: () => void
}

面试题

这是一道真实的面试题,题目如下

进入页面开始每秒发送一个请求,请求需携带定义的 state 值(最新的),仅可触发一次 setInterval 。

我们针对题目进行分析如下:

  1. 在进入页面时进行请求,必然需要使用**useEffect**
  2. 仅允许触发一次 setInterval,则说明该**useEffect**的依赖项为空数组
  3. 在每次请求时都需要保持最新的 state,但依赖项又需要为空数组,说明要使用到 **useRef** 来获得最新的值。

实现代码如下👇

function App() {
  
  const [state, setState] = useState(0)
  
  const getLastState = () => state
  
  const queryHandlerRef = useRef(getLastState)
  
  // 每次render都更新为最新的函数
  queryHandlerRef.current = getLastState
  
  useEffect(() => {
    const tick = () => {
      // 假设这里会使用 state 发送请求
      const s = queryHandlerRef.current()
      console.log(s)
    }
    
    let timer = setInterval(tick, 1000)
    return () => clearInterval(timer)
  }, [])
  
  return (
    <div>
      <div>{state}</div>
      <button onClick={() => setState(state + 1)}>state + 1</button>
    </div>
  )
}

效果如下👇

05|深入理解useRef

尽管我们满足了要求,但是代码耦合严重,我们可以将代码封装成一个自定义hook

function useInterval(callback: Function, delay: number) {
  const savedCallback = useRef(callback);

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, delay);
    return () => clearInterval(id);
  }, [delay]);
}

总结

OK,这一节我们结合useState,useEffect来讲useRef。主要讲了如下内容

  1. 介绍了useRef的使用场景
  2. 跨组件需要配合forwardRef来传递ref
  3. 父组件想要调用子组件内部的数据需要子组件通过useImperativeHandle来暴露。
  4. 通过分析一个面试题来解决问题,最后还将其封装成了一个自定义hook
转载自:https://juejin.cn/post/7123193200361570312
评论
请登录