第三六章 逃生舱口-使用 ref 引用值

2023-02-23  本文已影响0人  深圳都这么冷

使用 ref 引用值

当你想让一个组件“记住”一些信息,但又不想让这些信息触发新的渲染时,你可以使用 ref。

你将学习

  • 如何向组件添加引用
  • 如何更新 ref 的值
  • refs 与 state 有何不同
  • 如何安全地使用 refs

向您的组件添加引用

你可以通过从 React 导入 useRef Hook 来为你的组件添加一个 ref:

import { useRef } from 'react';

在您的组件内,调用 useRef Hook 并将您要引用的初始值作为唯一参数传递。 例如,这是对值 0 的引用:

const ref = useRef(0);

useRef 返回一个这样的对象:

{ 
  current: 0 // The value you passed to useRef
}

您可以通过 ref.current 属性访问该 ref 的当前值。 这个值是有意可变的,这意味着您可以读取和写入它。 它就像 React 不跟踪的组件的秘密口袋。 (这就是使它成为 React 单向数据流的“逃生舱门”的原因——更多内容见下文!)

在这里,一个按钮将在每次点击时增加 ref.current:

import { useRef } from 'react';

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

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

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

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

请注意,组件不会在每次增量时重新渲染。 与状态一样,refs 在重新渲染之间由 React 保留。 但是,设置状态会重新渲染组件。 更改 ref 不会!

示例:构建秒表

您可以将 refs 和 state 组合在一个组件中。 例如,让我们制作一个秒表,用户可以通过按下按钮来启动或停止。 为了显示自用户按下“开始”以来经过了多长时间,您需要跟踪按下“开始”按钮的时间以及当前时间。 此信息用于渲染,因此您将保持它的状态:

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

当用户按下“开始”时,您将使用 setInterval 每 10 毫秒更新一次时间:

import { useState } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);

  function handleStart() {
    // Start counting.
    setStartTime(Date.now());
    setNow(Date.now());

    setInterval(() => {
      // Update the current time every 10ms.
      setNow(Date.now());
    }, 10);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
    </>
  );
}

当按下“停止”按钮时,您需要取消现有interval,使其停止更新 now 状态变量。 您可以通过调用 clearInterval 来执行此操作,但您需要为其提供先前在用户按下 Start 时由 setInterval 调用返回的间隔 ID。 您需要将间隔 ID 保存在某处。 由于间隔 ID 不用于渲染,您可以将其保存在 ref 中:

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
      <button onClick={handleStop}>
        Stop
      </button>
    </>
  );
}

当一条信息用于渲染时,保持它的状态。 当一条信息仅由事件处理程序需要并且更改它不需要重新渲染时,使用 ref 可能更有效。

refs 和 state 的区别

也许你在想 refs 似乎没有 state 那么“严格”——你可以改变它们,而不是总是必须使用状态设置函数,例如。 但在大多数情况下,你会想要使用状态。 Refs 是一个你不会经常需要的“逃生口”。 以下是 state 和 refs 的比较:

refs state
useRef(initialValue) 返回 { current: initialValue } useState(initialValue) 返回状态变量的当前值和状态设置函数 ( [value, setValue])
更改时不会触发重新渲染。 当您更改它时,触发器会重新呈现。
可变——您可以在渲染过程之外修改和更新 current 的值。 “不可变”——你必须使用状态设置函数来修改状态变量来排队重新渲染。
您不应该在渲染期间读取(或写入)current 值。 您可以随时读取状态。 但是,每个渲染都有自己的状态快照,不会改变。

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

import { 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,所以为它使用状态值是有意义的。 当使用 setCount() 设置计数器的值时,React 重新渲染组件并且屏幕更新以反映新的计数。

如果你试图用 ref 来实现它,React 永远不会重新渲染组件,所以你永远不会看到计数发生变化! 查看单击此按钮如何不更新其文本:

import { useRef } from 'react';

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

  function handleClick() {
    // This doesn't re-render the component!
    countRef.current = countRef.current + 1;
  }

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

这就是为什么在渲染期间读取 ref.current 会导致代码不可靠。 如果需要,请改用状态。

深度阅读:useRef内部如何工作?

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

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

在第一次渲染期间,useRef 返回 { current: initialValue }。 该对象由 React 存储,因此在下一次渲染期间将返回相同的对象。 请注意此示例中未使用状态设置器的方式。 这是不必要的,因为 useRef 总是需要返回同一个对象!

React 提供了 useRef 的内置版本,因为它在实践中很常见。 但是您可以将其视为没有设置器的常规状态变量。 如果你熟悉面向对象的编程,refs 可能会让你想起实例字段——但你写的不是 this.something 而是somethingRef.current。

何时使用ref

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

如果您的组件需要存储一些值,但不影响渲染逻辑,请选择 refs。

ref的最佳实践

遵循这些原则将使您的组件更具可预测性:

React 状态的限制不适用于 refs。 例如,状态就像每个渲染的快照,不会同步更新。 但是当你改变 ref 的当前值时,它会立即改变:

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

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

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

引用和 DOM

您可以将 ref 指向任何值。 但是,ref 最常见的用例是访问 DOM 元素。 例如,如果您想以编程方式聚焦输入,这会很方便。 当你将 ref 传递给 JSX 中的 ref 属性时,如 <div ref={myRef}>,React 会将相应的 DOM 元素放入 myRef.current 中。 您可以在使用 Refs 操作 DOM 中阅读更多相关信息。

回顾

上一篇下一篇

猜你喜欢

热点阅读