sunilwang

V1

2022/07/26阅读:25主题:绿意

了解 useRef 一篇就够了

作者简介

王澍:一个会写Node.js的全栈工程师!58本地服务FE公众号小编;Picasso 开源项目负责人;

前言

最近小组分享,需要React HooksuseRef相关知识点,但是找了很多资料,没有一篇很系统的文章,所以我在网上整合了一些资料,带你一起由浅入深了解一下的useRef

useRef 是什么

useRefReact 16.8 新增特性的一个 Hook方法,当然React Hook中也包含了非常丰富的API方法,本文只针对useRef讲解。

那么useRef到底是什么?

举个例子:

import { useRef } from 'react';

const ref = useRef(0);

ref.current === 0 // true

  • 返回一个可变的 ref 对象,该对象只有个 .current 属性,初始值为传入的参数( initialValue )。
  • 返回的 ref 对象在组件的整个生命周期内保持不变。
  • 当更新 current 值时并不会 re-render ,这是与 useState 不同的地方。
  • useRef 类似于类组件的 this。

通俗点说useRef就像是可以在 .current 属性中保存一个可变值的“盒子”。

使用

看到useRef 的 Ref,肯定会想到 Class 中的 createRef,他们大部分用法基本一致,都是可以存变量或者Dom节点,具体区别,本文后面有具体介绍。

存变量

你可以通过 ref.current 属性访问这个 ref 的当前变量。这个变量是有意设置为可变的。这意味着您可以对它进行读写操作。

例子1:

import React, { useState, useRef, useCallback } from 'react'

export default function StopWatch({
    const [now, setNow] = useState(Date.now())
    const ref = useRef()

    const handleStart = useCallback(() => {
        ref.current = setInterval(() => {
            setNow(Date.now())
        }, 1000)
    }, [])

     const handleStop = useCallback(() => {
         clearInterval(ref.current)
     }, [])

    return (
        <>
            <h1>Now Time : {now}</h1>
            <button onClick={handleStart}>Start</button>
            <button onClick={handleStop}>Stop</button>
        </>

    )
}

上面案例中,我们使用 ref 存储 setInterval 返回的ID,需要清除时,我们只需要clearInterval(ref.current)就可以了。


例子2:


import React, { useRef } from 'react';

export default function ClickWatch({
  let ref = useRef(0);

    const handleClick = useCallback(() => {
        ref.current = ref.current + 1;
        alert('You clicked ' + ref.current + ' times!');
    }, [])

  return (
    <button onClick={handleClick}>
      Click me!
    </button>

  );
}

上面例子中,按钮将在每次点击后增加 ref.current

ref 指向一个数字,但是,像 state 一样,ref 可以指向任何东西:比如字符串、对象甚至函数。与 state 不同的是,ref 是一个带有当前属性的普通 JavaScript 对象,可以读取和修改。

注意:组件不会在每次递增时都 re-renderstate 一样,refre-render 之间被 React 保留。然而,setState将会 re-render 组件,而设置 ref 则不会。

存Dom节点

useRef获取React JSX中的DOM元素,获取后你就可以控制DOM的任何东西了。


import React, { useRef, useCallback } from 'react'

export default function TextInputWithFocusButton({
    const inputEl = useRef()

    const handleFocus = useCallback(() => {
        // `current` 指向已挂载到 DOM 上的文本输入元素
       inputEl.current.focus()
    }, [])

    return (
        <div>
            <input ref={inputEl} type="text" />
            <button onClick={handleFocus}>Focus the input</button>
        </div>

    )
}

什么时候需要使用 useRef

通常,当你的组件需要“跳出” React 并去与外部 API 通信时,你将会使用到 ref,通常的情况是使用不会影响组件外观的浏览器 API。 以下是一些比较罕见的情况:

  • 存储 timeout ID
  • 存储和操作 DOM 元素
  • 存储不需要计算 JSX 的其他对象

另外,如果你的组件需要存储一些值,但是这些值不会影响到 render 的逻辑,你可以选择使用 ref 来存储。

ref 的最佳实践

ref 的最佳实践会有下面几点,遵循下面的原则会使你的组件更具有可预测性:

  • 【把 ref 视为逃生舱】:当你使用外部系统或浏览器 API 时,ref 是一个很有用的方式。如果您的大部分应用程序逻辑和数据流依赖于 ref,您可能需要重新考虑您的方法是否正确,因为 ref 的可变性可能会使你的逻辑或数据流变得不好预测。

  • 【不要在 render 期间读取或写入 ref.current: 如果你在渲染过程中需要一些数据,请不要使用 ref,而是改用 state。由于 React 不知道 ref.current 是何时发生变化的,在渲染时读取它会使组件的行为难以预测。

React state 的限制不适用于 ref。例如,state 就像每次 render 的快照,并且不会同步更新。但是当你改变 ref 的当前值时,它会立即改变:

ref.current = 5;
console.log(ref.current); // 5

这是因为 ref 本质上是一个普通的 JavaScript 对象,所以它的行为就像这样。

当你使用 ref 时,你也不需要担心避免突变。只要你正在突变的对象不用于渲染,React 就不会关心你对 ref 或它的内容做什么。

useRef 和 setState 的对比

useRef setState
useRef(initialValue) 返回 { current: initialValue } useState(initialValue) 返回 state 的当前值和一个状态设置函数 ( [value, setValue])
当你改变它的时候不会触发 re-render 当你改变它的时候会触发 re-render
“可变的”:你可以在 rendering 过程之外修改和更新 current 的值 “不可变的”:你必须使用状态设置函数去修改状态,排队重新 re-render
你不应该在 rendering 过程中读或写 current 的值 你可以在任何时间读取 state,然而,每次 re-render 有它自己不会变化的 state 快照

这是一个使用状态实现的计数器按钮:

import React, { useState } from 'react';

export default function Counter({
  const [count, setCount] = useState(0);

  function handleClick({
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      You clicked {count} times
    </button>

  );
}

因为显示了 count 的值,所以使用 state 是有意义的。当使用 setCount() 设置计数器的值时,React 会 re-render 组件并更新屏幕以展示新的 count。

如果你试图用 ref 来实现它,React 永远不会重新渲染组件,所以你永远不会看到计数变化!

import React, { useRef } from 'react';

export default function Counter({
  let countRef = useRef(0);

  function handleClick({
    // 这里不会 re-render 这个组件!
    countRef.current = countRef.current + 1;
  }

  return (
    <button onClick={handleClick}>
      You clicked {countRef.current} times
    </button>

  );
}

不管怎么点击按钮,页面始终会显示 You clicked 0 times

useRef 和 createRef 的区别

useRef 仅能用在 FunctionComponent,而createRef 仅能用在 ClassComponent

回顾 createRef 使用

import React, { Component, createRef } from 'react'

export default class TextInputWithFocusButton extends Component {
    constructor(args) {
        super(args)

        this.inputEl = createRef()
    }

    handleFocus = () => {
        // `current` 指向已挂载到 DOM 上的文本输入元素
        this.inputEl.current.focus()
    }

    render() {
        return (
            <div>
                <input ref={this.inputEl} type="text" />
                <button onClick={this.handleFocus}>Focus the input</button>
            </div>

        )
    }
}

createRef 在 FunctionComponent 使用

有同学肯定好奇,如果 createRef 用在 FunctionComponent 中会怎样?

其实 createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。如果你还不太理解, 没关系. 我们再用一个例子来加深理解 createRefuseRef 的不同之处。

import React, { useRef, createRef, useState } from 'react'

export default function useRefAndCreateRef({
    const [count, setCount] = useState(1)
    const refFromUseRef = useRef()
    const refFromCreateRef = createRef()

    if (!refFromUseRef.current) {
        refFromUseRef.current = count
    }

    if (!refFromCreateRef.current) {
        refFromCreateRef.current = count
    }

    const handleClick = () => {
        setCount((prev) => prev + 1)
    }

    return (
        <div>
            <p>count:{count}</p>
            <p>refFromUseRef{refFromUseRef.current}</p>
            <p>refFromCreateRef{refFromCreateRef.current}</p>
            <button onClick={handleClick}>Cause re-render</button>
        </div>

    )
}

点击按钮后就算组件重新渲染,由于 refFromUseRef 的值一直存在(类似于 this ) , 无法重新赋值,所以一直都是1,而 createRef 每次都会返回一个新的引用,所以每次更新countrefFromCreateRef.current都最新的值。

useRef 的原理

尽管 useStateuseRef 都是 React 提供的,但原则上 useRef 可以在 useState 之上实现。你可以想象在 React 内部, useRef 是这样实现的:

function useRef(initialValue{
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}

第一次渲染时,useRef 返回了 { current: initialValue }。这个对象会被 React 存起来,所以在下一次渲染时,将返回同样的对象,注意这里 state 的设置函数 unused 在这个例子中,是没有使用到的。它是不需要的,因为 useRef 总是只需要返回同一个对象!

useRef 的源码

在源码中去除多余的代码,其实很简单,主要分为Mount阶段Update阶段.

Mount阶段

在这个阶段, useRef 和其他Hook⼀样创建⼀个Hook对象,然后创建⼀个 {current: initialValue} 的值,缓存到HookmemoizedState 属性,并返回该值。

function mountRef<T>(initialValue: T): {|current: T|} {
  const hook = mountWorkInProgressHook();

  if (enableUseRefAccessWarning) {
    if (__DEV__) {
      //...
    } else {
      const ref = {current: initialValue};
      hook.memoizedState = ref;
      return ref;
    }
  } else {
    const ref = {current: initialValue};
    hook.memoizedState = ref;
    return ref;
  }
}

Update阶段

这个阶段很懒,直接从Hook实例中返回之前缓存的值。

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

这样看是不是非常简单呢?

总结

  • ref 是一个逃生舱,它可以保存不被用于 render 的数据,你不会经常需要它们。
  • ref 是一个纯 JavaScript 对象,它有一个名为 current 的属性,你可以读取或设置它。
  • 你可以通过调用 useRef 这个 hook 让 React 生成一个 ref
  • 类似 state,ref 让你在组件 re-render 之间保存数据。
  • state 不同的是,设置 refcurrent 属性不会触发 re-render
  • 不要在 render 的过程中读或写 ref.current,这会使你的组件难以预测。

参考

https://juejin.cn/post/7023983265870512135


LBG开源项目推广:

还在手写 HTML 和 CSS 吗?
还在写布局吗?
快用 Picasso 吧,Picasso 一键生成高可用的前端代码,让你有更多的时间去沉淀和成长,欢迎Star

开源项目地址:https://github.com/wuba/Picasso
官网地址:https://picassoui.58.com

分类:

前端

标签:

React.js

作者介绍

sunilwang
V1