美文网首页
第三七章 逃生舱口-使用 ref 操作 DOM

第三七章 逃生舱口-使用 ref 操作 DOM

作者: 深圳都这么冷 | 来源:发表于2023-02-23 14:47 被阅读0次

使用 ref 操作 DOM

React 会自动更新 DOM 以匹配您的渲染输出,因此您的组件不需要经常操作它。 然而,有时您可能需要访问由 React 管理的 DOM 元素——例如,聚焦一个节点,滚动到它,或者测量它的大小和位置。 在 React 中没有内置的方法来做这些事情,所以你需要一个 DOM 节点的引用。

你将学习

  • 如何使用 ref 属性访问由 React 管理的 DOM 节点
  • ref JSX 属性与 useRef Hook 的关系
  • 如何访问另一个组件的 DOM 节点
  • 在哪些情况下修改 React 管理的 DOM 是安全的

获取节点的引用

要访问由 React 管理的 DOM 节点,首先,导入 useRef Hook:

import { useRef } from 'react';

然后,使用它在您的组件内声明一个 ref:

const myRef = useRef(null);

最后,将其作为 ref 属性传递给 DOM 节点:

<div ref={myRef}>

useRef Hook 返回一个对象,该对象具有一个名为 current 的属性。 最初,myRef.current 将为空。 当 React 为这个 <div> 创建一个 DOM 节点时,React 会把这个节点的引用放到 myRef.current 中。 然后,您可以从事件处理程序访问此 DOM 节点,并使用在其上定义的内置浏览器 API。

// You can use any browser APIs, for example:
myRef.current.scrollIntoView();

示例:聚焦文本输入

在此示例中,单击按钮将聚焦输入:

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

要实现这一点:

  1. 使用 useRef Hook 声明 inputRef。
  2. 将其作为 <input ref={inputRef}> 传递。 这告诉 React 将这个 <input> 的 DOM 节点放入 inputRef.current。
  3. 在 handleClick 函数中,从 inputRef.current 读取输入 DOM 节点并使用 inputRef.current.focus() 对其调用 focus()。
  4. 使用 onClick 将 handleClick 事件处理程序传递给 <button>。

虽然 DOM 操作是 ref 最常见的用例,但 useRef Hook 可用于存储 React 之外的其他内容,例如计时器 ID。 与状态类似,refs 保留在渲染之间。 Refs 就像状态变量,当你设置它们时不会触发重新渲染。 有关 refs 的介绍,请参阅使用 Refs 引用值。

示例:滚动到一个元素

一个组件中可以有多个 ref。 在此示例中,有一个包含三个图像的轮播。 每个按钮通过调用浏览器 scrollIntoView() 方法将图像居中对应的 DOM 节点:

import { useRef } from 'react';

export default function CatFriends() {
  const firstCatRef = useRef(null);
  const secondCatRef = useRef(null);
  const thirdCatRef = useRef(null);

  function handleScrollToFirstCat() {
    firstCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function handleScrollToSecondCat() {
    secondCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function handleScrollToThirdCat() {
    thirdCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  return (
    <>
      <nav>
        <button onClick={handleScrollToFirstCat}>
          Tom
        </button>
        <button onClick={handleScrollToSecondCat}>
          Maru
        </button>
        <button onClick={handleScrollToThirdCat}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul>
          <li>
            <img
              src="https://placekitten.com/g/200/200"
              alt="Tom"
              ref={firstCatRef}
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/300/200"
              alt="Maru"
              ref={secondCatRef}
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/250/200"
              alt="Jellylorum"
              ref={thirdCatRef}
            />
          </li>
        </ul>
      </div>
    </>
  );
}

深度阅读:如何使用 ref 回调管理 refs 列表

在上面的示例中,有预定义数量的 refs。 然而,有时您可能需要对列表中的每个项目进行引用,而您不知道您将拥有多少。 这样的事情是行不通的:

<ul>
  {items.map((item) => {
    // Doesn't work!
    const ref = useRef(null);
    return <li ref={ref} />;
  })}
</ul>

这是因为 Hooks 只能在组件的顶层调用。 您不能在循环、条件或 map() 调用中调用 useRef。

解决此问题的一种可能方法是获取对其父元素的单个引用,然后使用 querySelectorAll 等 DOM 操作方法从中“查找”各个子节点。 但是,这很脆弱,如果您的 DOM 结构发生变化,可能会崩溃。

另一种解决方案是将函数传递给 ref 属性。 这称为 ref 回调。 React 会在设置 ref 时使用 DOM 节点调用你的 ref 回调,在清除它时使用 null。 这使您可以维护自己的数组或 Map,并通过其索引或某种 ID 访问任何 ref。

此示例显示如何使用此方法滚动到长列表中的任意节点:

import { useRef } from 'react';

export default function CatFriends() {
  const itemsRef = useRef(null);

  function scrollToId(itemId) {
    const map = getMap();
    const node = map.get(itemId);
    node.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // Initialize the Map on first usage.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToId(0)}>
          Tom
        </button>
        <button onClick={() => scrollToId(5)}>
          Maru
        </button>
        <button onClick={() => scrollToId(9)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul>
          {catList.map(cat => (
            <li
              key={cat.id}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat.id, node);
                } else {
                  map.delete(cat.id);
                }
              }}
            >
              <img
                src={cat.imageUrl}
                alt={'Cat #' + cat.id}
              />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

const catList = [];
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: 'https://placekitten.com/250/200?image=' + i
  });
}

在此示例中,itemsRef 不包含单个 DOM 节点。 相反,它持有从项目 ID 到 DOM 节点的映射。 (Refs 可以包含任何值!)每个列表项上的 ref 回调负责更新 Map:

<li
  key={cat.id}
  ref={node => {
    const map = getMap();
    if (node) {
      // Add to the Map
      map.set(cat.id, node);
    } else {
      // Remove from the Map
      map.delete(cat.id);
    }
  }}
>

这使您可以稍后从 Map 中读取单个 DOM 节点。

访问另一个组件的 DOM 节点

当您将 ref 放在输出浏览器元素(如 <input />)的内置组件上时,React 会将该 ref 的当前属性设置为相应的 DOM 节点(例如浏览器中的实际 <input />)。

但是,如果您尝试将 ref 放在您自己的组件上,例如 <MyInput />,默认情况下您将获得 null。 这是一个演示它的例子。 请注意单击按钮如何不聚焦输入:

import { useRef } from 'react';

function MyInput(props) {
  return <input {...props} />;
}

export default function MyForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

为了帮助您注意到这个问题,React 还会向控制台打印错误:

X console
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

发生这种情况是因为默认情况下 React 不允许组件访问其他组件的 DOM 节点。 连自己的孩子都不放过! 这是故意的。 Refs 是一个逃生口,应该谨慎使用。 手动操作另一个组件的 DOM 节点会使您的代码更加脆弱。

相反,想要公开其 DOM 节点的组件必须选择加入该行为。 一个组件可以指定它“转发”它的 ref 给它的一个孩子。 以下是 MyInput 如何使用 forwardRef API:

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

它是这样工作的:

  1. <MyInput ref={inputRef} /> 告诉 React 将相应的 DOM 节点放入 inputRef.current 中。 然而,这取决于 MyInput 组件选择加入——默认情况下,它不会。
  2. MyInput 组件是使用 forwardRef 声明的。 这选择它从上面接收 inputRef 作为在 props 之后声明的第二个 ref 参数。
  3. MyInput 本身将它收到的 ref 传递给其中的 <input> 。

现在单击按钮以聚焦输入工作:

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

在设计系统中,将它们的引用转发到它们的 DOM 节点是一种常见的模式,用于低级组件(如按钮、输入等)。 另一方面,表单、列表或页面部分等高级组件通常不会公开其 DOM 节点,以避免对 DOM 结构的意外依赖。

深度阅读:使用命令句柄公开 API 的子集

在上面的示例中,MyInput 暴露了原始 DOM 输入元素。 这让父组件在其上调用 focus() 。 然而,这也让父组件可以做一些其他事情——例如,改变它的 CSS 样式。 在不常见的情况下,您可能希望限制暴露的功能。 您 可以使用 useImperativeHandle 来做到这一点:

import {
  forwardRef,
  useRef,
  useImperativeHandle
} from 'react';

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Only expose focus and nothing else
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} ref={realInputRef} />;
});

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

在这里,MyInput 中的 realInputRef 保存实际的输入 DOM 节点。 但是,useImperativeHandle 指示 React 提供您自己的特殊对象作为父组件的 ref 值。 所以 Form 组件内部的 inputRef.current 只会有 focus 方法。 在这种情况下,引用“句柄”不是 DOM 节点,而是您在 useImperativeHandle 调用中创建的自定义对象。

当 React 附加 refs 时

在 React 中,每次更新都分为两个阶段:

  • 在渲染期间,React 调用您的组件来确定屏幕上应该显示什么。
  • 在提交期间,React 将更改应用到 DOM。

通常,您不想在渲染期间访问 refs。 这也适用于持有 DOM 节点的 refs。 在第一次渲染期间,DOM 节点尚未创建,因此 ref.current 将为空。 并且在渲染更新的过程中,DOM 节点还没有更新。 所以现在读它们还为时过早。

React 在提交期间设置 ref.current。 在更新 DOM 之前,React 将受影响的 ref.current 值设置为 null。 更新 DOM 后,React 立即将它们设置为相应的 DOM 节点。

通常,您将从事件处理程序访问 refs。 如果你想用 ref 做一些事情,但没有特定的事件可以做,你可能需要一个 Effect。 我们将在下一页讨论效果。

深度阅读:使用 flushSync 同步刷新状态

考虑这样的代码,它添加了一个新的待办事项并将屏幕向下滚动到列表的最后一个子项。 请注意,由于某种原因,它总是滚动到最后添加的待办事项之前的待办事项:

import { useState, useRef } from 'react';

export default function TodoList() {
  const listRef = useRef(null);
  const [text, setText] = useState('');
  const [todos, setTodos] = useState(
    initialTodos
  );

  function handleAdd() {
    const newTodo = { id: nextId++, text: text };
    setText('');
    setTodos([ ...todos, newTodo]);
    listRef.current.lastChild.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest'
    });
  }

  return (
    <>
      <button onClick={handleAdd}>
        Add
      </button>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <ul ref={listRef}>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}

let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {
  initialTodos.push({
    id: nextId++,
    text: 'Todo #' + (i + 1)
  });
}

问题在于这两行:

setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();

在 React 中,状态更新是排队的。 通常,这就是您想要的。 但是,这里会导致问题,因为 setTodos 不会立即更新 DOM。 因此,当您将列表滚动到最后一个元素时,尚未添加待办事项。 这就是为什么滚动总是“滞后”一个项目。

要解决此问题,您可以强制 React 同步更新(“刷新”)DOM。 为此,从 react-dom 导入 flushSync 并将状态更新包装到 flushSync 调用中:

flushSync(() => {
  setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

这将指示 React 在 flushSync 中包装的代码执行后立即同步更新 DOM。 因此,当您尝试滚动到最后一个待办事项时,它已经在 DOM 中了。

使用 refs 操作 DOM 的最佳实践

refs是一个逃生舱口。 你应该只在你必须“走出 React”的时候使用它们。 这方面的常见示例包括管理焦点、滚动位置或调用 React 未公开的浏览器 API。

如果您坚持聚焦和滚动等非破坏性操作,您应该不会遇到任何问题。 但是,如果您尝试手动修改 DOM,则可能会与 React 所做的更改发生冲突。

为说明此问题,此示例包含一条欢迎消息和两个按钮。 第一个按钮使用条件渲染和状态切换它的存在,就像你通常在 React 中所做的那样。 第二个按钮使用 remove() DOM API 将其从 React 控制之外的 DOM 中强制删除。

尝试按几次“Toggle with setState”。 该消息应该消失并再次出现。 然后按“Remove from the DOM”。 这将强制删除它。 最后,按“Toggle with setState”:

import {useState, useRef} from 'react';

export default function Counter() {
  const [show, setShow] = useState(true);
  const ref = useRef(null);

  return (
    <div>
      <button
        onClick={() => {
          setShow(!show);
        }}>
        Toggle with setState
      </button>
      <button
        onClick={() => {
          ref.current.remove();
        }}>
        Remove from the DOM
      </button>
      {show && <p ref={ref}>Hello world</p>}
    </div>
  );
}

手动删除 DOM 元素后,尝试使用 setState 再次显示它会导致崩溃。 这是因为你已经改变了 DOM,而 React 不知道如何继续正确地管理它。

避免更改由 React 管理的 DOM 节点。 在由 React 管理的元素中修改、添加或删除子元素可能会导致不一致的视觉结果或像上面那样的崩溃。

但是,这并不意味着您根本无法做到。 这需要谨慎。 您可以安全地修改 React 没有理由更新的 DOM 部分。 例如,如果某些 <div> 在 JSX 中始终为空,React 将没有理由去触及它的子列表。 因此,在那里手动添加或删除元素是安全的。

回顾

  • Refs 是一个通用概念,但大多数情况下你会用它们来保存 DOM 元素。
  • 您通过传递 <div ref={myRef}> 指示 React 将 DOM 节点放入 myRef.current。
  • 通常,您将使用 refs 进行非破坏性操作,例如聚焦、滚动或测量 DOM 元素。
  • 默认情况下,组件不会公开其 DOM 节点。 您可以通过使用 forwardRef 并将第二个 ref 参数向下传递到特定节点来选择公开 DOM 节点。
  • 避免更改由 React 管理的 DOM 节点。
  • 如果你确实修改了由 React 管理的 DOM 节点,请修改 React 没有理由更新的部分。

相关文章

  • Vue中ref属性获取DOM元素和组件引用

    ref获取DOM元素vue中获取DOM元素不建议用js直接操作DOM,使用ref可达到操作DOM的效果写法:thi...

  • React学习笔记(三)

    React中的 ref 的使用 ref是一个引用,在React中使用ref来直接获取DOM元素从而操作DOM Re...

  • 7.Vue 操作dom

    Vue操作dom: 在Vue中获取dom,需要在dom上使用ref="名称",然后通过:this.$refs.名称...

  • vue零基础开发015——ref引用

    【ref模型】 【dom操作】 【ref引用计算】

  • 使用Ref进行DOM操作

    原因 React.js 并不能完全满足所有 DOM 操作需求,有些时候我们还是需要和 DOM 打交道。比如说想动态...

  • React 中 ref 的使用

    ref是reference的简写,它是一个引用,在react中,我们使用ref来操作DOM。 在react中,我们...

  • Vue 操作DOM节点

    ref $refs使用 $el DOM节点

  • Vue3.x ref属性

    获取DOM或者组件实例可以使用ref属性,写法和vue2.0需要区分开。 vue2.0的方式操作ref----数组...

  • ViewContainerRef 动态创建视图

    Angular DOM 操作 相关的APIs和类: 查询DOM节点template variable ref: 模...

  • React ref属性

    一、用途 ref属性允许我们使用react操作真实DOM 二、用法 ref属性的值是一个箭头函数,参数接收真实的D...

网友评论

      本文标题:第三七章 逃生舱口-使用 ref 操作 DOM

      本文链接:https://www.haomeiwen.com/subject/oiznkdtx.html