MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 使用 Refs 操作 DOM 和子组件

2022-01-272.0k 阅读

React 中的 Refs 简介

在 React 开发中,我们通常遵循“数据驱动视图”的理念,通过状态(state)和属性(props)的变化来更新 UI。然而,在某些特定情况下,我们需要直接操作 DOM 元素或者访问子组件的实例,这时就需要用到 Refs。Refs 为我们提供了一种在 React 组件中访问 DOM 元素或子组件实例的方式。

Refs 的创建

在 React 中,创建 Refs 有几种不同的方式。在 React 16.3 及更高版本中,推荐使用 React.createRef() 方法来创建 Refs。以下是一个简单的示例:

import React, { Component } from'react';

class MyComponent extends Component {
  constructor(props) {
    super(props);
    // 创建一个 Ref
    this.inputRef = React.createRef();
  }
  render() {
    return <input type="text" ref={this.inputRef} />;
  }
}

在上述代码中,我们在 MyComponent 的构造函数中使用 React.createRef() 创建了一个名为 inputRef 的 Ref。然后,我们将这个 Ref 赋值给了 input 元素的 ref 属性。这样,inputRef.current 就会指向该 input 元素的 DOM 节点。

在 React 16.3 之前,我们使用回调函数的方式来创建 Refs。如下所示:

import React, { Component } from'react';

class OldStyleRefComponent extends Component {
  constructor(props) {
    super(props);
    this.handleRef = (element) => {
      this.inputElement = element;
    };
  }
  render() {
    return <input type="text" ref={this.handleRef} />;
  }
}

在这里,ref 属性接收一个回调函数,当 input 元素挂载时,这个回调函数会被调用,并且将 input 元素的 DOM 节点作为参数传入。我们将这个 DOM 节点保存在 this.inputElement 中,以便后续使用。虽然这种方式仍然可用,但使用 React.createRef() 更加简洁和直观。

使用 Refs 操作 DOM

获取 DOM 元素的值

一旦我们创建了指向 DOM 元素的 Ref,就可以通过 ref.current 来获取该 DOM 元素,进而操作它。例如,获取输入框的值:

import React, { Component } from'react';

class GetInputValueComponent extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  handleClick = () => {
    const inputValue = this.inputRef.current.value;
    console.log('输入框的值是:', inputValue);
  };
  render() {
    return (
      <div>
        <input type="text" ref={this.inputRef} />
        <button onClick={this.handleClick}>获取输入值</button>
      </div>
    );
  }
}

在上述代码中,当用户点击按钮时,handleClick 方法会被调用。在这个方法中,我们通过 this.inputRef.current.value 获取了输入框的值,并在控制台打印出来。

聚焦 DOM 元素

另一个常见的操作是聚焦到某个 DOM 元素上。例如,在页面加载完成后,自动聚焦到输入框:

import React, { Component } from'react';

class AutoFocusComponent extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  componentDidMount() {
    this.inputRef.current.focus();
  }
  render() {
    return <input type="text" ref={this.inputRef} />;
  }
}

componentDidMount 生命周期方法中,我们通过 this.inputRef.current.focus() 让输入框自动获得焦点。componentDidMount 会在组件挂载后立即调用,确保此时 input 元素已经存在于 DOM 中,可以进行聚焦操作。

操作 DOM 样式

我们还可以通过 Refs 来操作 DOM 元素的样式。以下是一个改变按钮背景颜色的示例:

import React, { Component } from'react';

class ChangeButtonStyleComponent extends Component {
  constructor(props) {
    super(props);
    this.buttonRef = React.createRef();
  }
  handleClick = () => {
    this.buttonRef.current.style.backgroundColor ='red';
  };
  render() {
    return (
      <div>
        <button ref={this.buttonRef} onClick={this.handleClick}>点击改变背景色</button>
      </div>
    );
  }
}

当按钮被点击时,handleClick 方法通过 this.buttonRef.current.style.backgroundColor 将按钮的背景颜色设置为红色。

使用 Refs 访问子组件实例

调用子组件方法

有时候,我们需要在父组件中调用子组件的方法。通过 Refs 可以轻松实现这一点。假设我们有一个子组件 ChildComponent,它有一个 sayHello 方法:

import React, { Component } from'react';

class ChildComponent extends Component {
  sayHello = () => {
    console.log('Hello from ChildComponent!');
  };
  render() {
    return <div>子组件</div>;
  }
}

class ParentComponent extends Component {
  constructor(props) {
    super(props);
    this.childRef = React.createRef();
  }
  handleClick = () => {
    this.childRef.current.sayHello();
  };
  render() {
    return (
      <div>
        <ChildComponent ref={this.childRef} />
        <button onClick={this.handleClick}>调用子组件方法</button>
      </div>
    );
  }
}

ParentComponent 中,我们创建了一个指向 ChildComponent 的 Ref,并在按钮点击时通过 this.childRef.current.sayHello() 调用子组件的 sayHello 方法。

获取子组件状态

虽然直接获取子组件状态违背了 React 的单向数据流原则,但在某些特殊情况下可能是必要的。假设子组件 Counter 有一个状态 count

import React, { Component } from'react';

class Counter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  increment = () => {
    this.setState((prevState) => ({
      count: prevState.count + 1
    }));
  };
  render() {
    return (
      <div>
        <p>计数: {this.state.count}</p>
        <button onClick={this.increment}>增加计数</button>
      </div>
    );
  }
}

class ParentForStateComponent extends Component {
  constructor(props) {
    super(props);
    this.counterRef = React.createRef();
  }
  handleClick = () => {
    const childCount = this.counterRef.current.state.count;
    console.log('子组件的计数:', childCount);
  };
  render() {
    return (
      <div>
        <Counter ref={this.counterRef} />
        <button onClick={this.handleClick}>获取子组件计数</button>
      </div>
    );
  }
}

ParentForStateComponent 中,通过按钮点击,我们可以获取 Counter 子组件的 count 状态值。不过,这种做法应尽量避免,因为它破坏了 React 的数据流模式,使代码难以维护和调试。如果可能,应该通过 props 和回调函数来实现父子组件间的通信。

Refs 的注意事项

Ref 与函数式组件

函数式组件没有实例,因此不能直接在函数式组件上使用 Refs。如果需要在函数式组件中使用 Refs,可以使用 forwardRef 来转发 Refs。例如:

import React, { forwardRef } from'react';

const FunctionalComponent = forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

class ParentWithFunctionalComponent extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  handleClick = () => {
    const inputValue = this.inputRef.current.value;
    console.log('函数式组件输入框的值:', inputValue);
  };
  render() {
    return (
      <div>
        <FunctionalComponent ref={this.inputRef} />
        <button onClick={this.handleClick}>获取函数式组件输入值</button>
      </div>
    );
  }
}

在上述代码中,forwardRef 接收一个函数,该函数接收 propsref 作为参数。我们将 ref 传递给了 input 元素,使得父组件可以通过 Ref 访问函数式组件中的 input 元素。

Ref 的更新与卸载

当组件更新时,Ref 的 current 值不会自动更新。只有当 DOM 元素或子组件重新挂载时,ref.current 才会指向新的实例。例如,在一个包含输入框的组件中,如果输入框被替换(比如通过条件渲染),旧的 ref.current 仍然指向旧的输入框,直到新的输入框挂载。

当组件卸载时,React 会自动清空 Refs 的 current 值为 null。这确保了不会有无效的引用导致内存泄漏。例如,在 componentWillUnmount 生命周期方法中,我们不需要手动清空 Ref:

import React, { Component } from'react';

class UnmountComponent extends Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  componentWillUnmount() {
    // 不需要手动设置 this.inputRef.current = null;
    // React 会自动将 this.inputRef.current 设为 null
  }
  render() {
    return <input type="text" ref={this.inputRef} />;
  }
}

避免滥用 Refs

虽然 Refs 提供了强大的功能,但应该谨慎使用。过度使用 Refs 会破坏 React 的单向数据流和声明式编程模型,使代码难以理解和维护。在大多数情况下,通过状态和属性的传递可以满足需求,只有在真正需要直接操作 DOM 或子组件实例时才使用 Refs。

总结 Refs 在 React 中的应用

Refs 在 React 中为我们提供了一种突破常规数据流的方式,使得我们能够直接与 DOM 元素和子组件实例进行交互。通过 React.createRef() 或回调函数的方式创建 Refs,我们可以轻松地获取 DOM 元素的值、聚焦元素、操作样式,以及调用子组件的方法和获取子组件状态。然而,使用 Refs 时需要注意其适用场景,避免滥用,以保持 React 应用的可维护性和可扩展性。在函数式组件中使用 Refs 时,要借助 forwardRef 来实现。同时,理解 Ref 的更新和卸载机制也有助于我们编写更加健壮的代码。通过合理运用 Refs,我们可以在遵循 React 核心原则的基础上,实现一些特殊的交互需求,提升用户体验。