首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >26届秋招前端面试题系列 | React篇(上)

26届秋招前端面试题系列 | React篇(上)

作者头像
爱学英语的程序媛
发布2026-09-15 11:16:47
发布2026-09-15 11:16:47
10
举报

哈喽小伙伴们,上一期文章为大家分享了vue相关的面试题,今天,我们一起来看看react相关的内容~

因为React的内容比较多,所以我们分为上下两期文章,今天先来看第一期~

在开始之前,我们先来看一张思维导图:

react,它跟vue都作为一种前端的框架,它在语法和使用场景上有什么不一样呢?今天的文章将会告诉你答案。

基础使用

1、React 组件(函数组件 vs 类组件)

在React中,组件的定义方式有两种,一种是函数组件,另外一种是类组件。

函数组件是最简洁的组件定义方式,本质上是一个返回 React 元素的 JavaScript 函数。

代码语言:javascript
复制
// 最简单的函数组件
function Welcome() {
  return <h1>Hello, React!</h1>;
}
// 使用箭头函数定义
const Greeting = () => {
  return <p>Welcome to my app</p>;
};

它的特点有以下三个:

1)、语法简洁,易于理解和编写

2)、无状态组件(早期只能作为纯展示组件,React 16.8 后可通过 Hooks 拥有状态)

3)、推荐在大多数场景下使用,尤其是纯展示或逻辑简单的组件

还有一种组件就是类组件,它是基于 ES6 类语法定义的组件,需要继承 React.Component 并实现 render() 方法。

代码语言:javascript
复制
import React from 'react';
class Welcome extends React.Component {
  // 必须实现 render 方法,返回 React 元素
  render() {
    return <h1>Hello, React!</h1>;
  }
}

它的特点也有以下三个:

1)、可以拥有内部状态(通过 this.state

2)、可以使用生命周期方法(如 componentDidMountcomponentDidUpdate

3)、语法相对繁琐,在 Hooks 出现后使用场景逐渐减少

它的状态管理一般是通过接收props来实现的。

代码语言:javascript
复制
class UserProfile extends React.Component {
  // 初始化状态
  constructor(props) {
    super(props);
    this.state = {
      isOnline: false
    };
  }

  // 生命周期方法
  componentDidMount() {
    // 模拟API请求更新状态
    setTimeout(() => {
      this.setState({ isOnline: true });
    }, 1000);
  }

  render() {
    return (
      <div>
        <h2>{this.props.username}</h2>
        <p>Status: {this.state.isOnline ? 'Online' : 'Offline'}</p>
      </div>
    );
  }
}
// 使用组件
<UserProfile username="Bob" />

2、Props 与 State

接下来我们来看一看react中的Props 与 State

在react中,Props 和 State 是管理组件数据的两个核心概念,它们共同支撑了组件的渲染和交互逻辑,同时遵循单向数据流原则

1)、props表示父组件传递到子组件的数据。它有两个特性:只读性(子组件不能修改父组件传递过去的数据)和单向传递(数据只能从父组件流向子组件)

代码语言:javascript
复制
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Child count={count} />
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}
function Child(props) {
  return <p>Count from parent: {props.count}</p>;
}

2)、state表示组件内部管理的数据,用于记录组件自身的动态变化,仅在组件内部有效。

它的核心特性有三个:

私有性:state 是组件的私有数据,只能在组件内部修改,父组件无法直接访问或修改子组件的 state

响应式:state 变化时,组件会重新渲染(render 方法重新执行),更新 UI。

初始化:这里分为函数组件和类组件两种情况:

  • 函数组件:通过 useState Hook 初始化(React 16.8+)。
  • 类组件:在 constructor 中通过 this.state 初始化。

函数组件中的state:

代码语言:javascript
复制
import { useState } from 'react';
function Counter() {
  // 初始化 state:count 为状态变量,setCount 为更新函数
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>加1</button>
    </div>
  );
}

类组件中的state:

代码语言:javascript
复制
class Counter extends React.Component {
  // 初始化 state
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    return (
      <div>
        <p>当前计数:{this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          加1
        </button>
      </div>
    );
  }
}

那props和state是如何协作的呢?

  1. 父组件通过 props 将数据传递给子组件。
  2. 子组件通过调用父组件传递的回调函数(props 中的函数),通知父组件修改数据。
  3. 父组件修改自身 state 后,重新渲染并将新数据通过 props 传递给子组件,触发子组件更新

下面这段代码展示了子组件是如何通知父组件更新数据的过程:

代码语言:javascript
复制
function Parent() {
  const [text, setText] = useState('');

  // 父组件定义修改函数,通过 props 传递给子组件
  const handleTextChange = (newText) => {
    setText(newText);
  };

  return (
    <div>
      <p>父组件显示:{text}</p>
      <Child onTextChange={handleTextChange} />
    </div>
  );
}
function Child(props) {
  const [inputValue, setInputValue] = useState('');

  const handleInput = (e) => {
    setInputValue(e.target.value);
    // 调用父组件传递的回调,传递新值
    props.onTextChange(e.target.value);
  };

  return <input value={inputValue} onChange={handleInput} />;
}

3、生命周期(类组件钩子 + 函数组件 Hooks 对应)

接下来,我们一起来看看react的生命周期函数:

因为react的组件分为函数组件和类组件,两者的生命周期略有不同。类组件通过生命周期钩子函数管理这些阶段,而函数组件则通过 Hooks(如 useEffect)实现类似功能。

接下来,我们先来看类组件的生命周期:

类组件的生命周期可分为三个核心阶段挂载(Mounting)更新(Updating)卸载(Unmounting),每个阶段对应特定的钩子函数。

1. 挂载阶段(组件创建并插入 DOM)

组件首次渲染到 DOM 中的过程,只执行一次。

  • constructor():组件初始化时调用,用于设置初始 

state 和绑定事件处理函数。

  • static getDerivedStateFromProps(props, state):根据 

props 动态更新 state(极少使用,优先考虑更简单的方案)。

  • render():必须实现的方法,返回组件的 JSX 结构(纯函数,不修改状态)。
  • componentDidMount():组件挂载到 DOM 后调用,常用于执行副作用(如数据请求、事件监听、初始化第三方库)。
代码语言:javascript
复制
class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = { data: null };
    console.log('constructor:初始化');
  }
  componentDidMount() {
    console.log('componentDidMount:组件已挂载');
    // 示例:请求数据
    fetch('/api/data').then(res => res.json()).then(data => {
      this.setState({ data });
    });
  }
  render() {
    console.log('render:渲染组件');
    return <div>{this.state.data ? this.state.data : '加载中...'}</div>;
  }
}
2. 更新阶段(组件重新渲染)

当组件的 props 或 state 变化时触发,可能多次执行。

  • static getDerivedStateFromProps(props, state):同挂载阶段,用于根据 props 更新 state
  • shouldComponentUpdate(nextProps, nextState):控制组件是否需要重新渲染(返回 false 则跳过更新,用于性能优化)。
  • render():重新渲染组件。
  • getSnapshotBeforeUpdate(prevProps, prevState):在 DOM 更新前获取快照(如滚动位置),返回值会作为 componentDidUpdate 的第三个参数。
  • componentDidUpdate(prevProps, prevState, snapshot):DOM 更新后调用,可根据前后 props/state 的差异执行操作(如再次请求数据)。
代码语言:javascript
复制
componentDidUpdate(prevProps) {
  // 当 props.id 变化时,重新请求数据
  if (this.props.id !== prevProps.id) {
    fetch(`/api/data/${this.props.id}`).then(res => res.json()).then(data => {
      this.setState({ data });
    });
  }
}
3. 卸载阶段(组件从 DOM 中移除)

组件被销毁时执行,只执行一次。

  • componentWillUnmount():组件卸载前调用,用于清理副作用(如移除事件监听、取消定时器、终止网络请求)。
代码语言:javascript
复制
componentDidMount() {
  // 添加事件监听
  window.addEventListener('resize', this.handleResize);
  // 设置定时器
  this.timer = setInterval(() => {
    this.setState({ time: new Date().toLocaleTimeString() });
  }, 1000);
}
componentWillUnmount() {
  // 清理:移除事件监听和定时器
  window.removeEventListener('resize', this.handleResize);
  clearInterval(this.timer);
}

接下来,来看函数组件的处理方式:

函数组件本身没有生命周期钩子,但通过 useEffect Hook 可以模拟类组件的所有生命周期行为,同时支持更灵活的逻辑组合。

首先来看useEffect的基本用法:

代码语言:javascript
复制
 useEffect(() => {
  // 副作用逻辑(如数据请求、事件监听)

  // 清理函数(可选):组件卸载或依赖变化时执行
  return () => {
    // 清理操作(如移除监听、取消定时器)
  };
}, [dependencies]); // 依赖数组:当数组中的值变化时,重新执行副作用

挂载阶段:依赖数组传空数组 [],表示副作用只在组件挂载后执行一次。

代码语言:javascript
复制
import { useState, useEffect } from 'react';
function Example() {
  const [data, setData] = useState(null);
  // 模拟 componentDidMount
  useEffect(() => {
    console.log('组件已挂载');
    // 请求数据
    fetch('/api/data').then(res => res.json()).then(data => {
      setData(data);
    });
  }, []); // 空依赖:只执行一次
  return <div>{data ? data : '加载中...'}</div>;
}

更新阶段:依赖数组传入具体变量,当这些变量变化时,执行副作用(模拟 componentDidUpdate

代码语言:javascript
复制
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  // 当 userId 变化时,重新请求数据(模拟 componentDidUpdate)
  useEffect(() => {
    console.log('userId 变化,重新请求数据');
    fetch(`/api/user/${userId}`).then(res => res.json()).then(user => {
      setUser(user);
    });
  }, [userId]); // 依赖 userId:userId 变化时执行
  return <div>{user ? user.name : '加载中...'}</div>;
}

若不传入依赖数组,useEffect 会在每次渲染后都执行(不推荐,可能导致性能问题)。

卸载阶段:通过 useEffect 的清理函数模拟,清理函数在组件卸载或依赖变化时执行。

代码语言:javascript
复制
function Timer() {
  const [time, setTime] = useState(new Date().toLocaleTimeString());
  useEffect(() => {
    // 设置定时器
    const timer = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);
    // 清理函数:模拟 componentWillUnmount
    return () => {
      clearInterval(timer); // 组件卸载时清除定时器
    };
  }, []); // 空依赖:只设置一次定时器
  return <div>当前时间:{time}</div>;
}

useEffect 也可以在一个组件中多次使用,实现逻辑拆分,比类组件的钩子更灵活。

代码语言:javascript
复制
function ComplexComponent({ id }) {
  // 1. 模拟 componentDidMount:初始化事件监听
  useEffect(() => {
    const handleResize = () => console.log('窗口大小变化');
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  // 2. 模拟 componentDidUpdate:id 变化时请求数据
  useEffect(() => {
    fetch(`/api/item/${id}`).then(res => res.json()).then(data => {
      console.log('数据更新', data);
    });
  }, [id]);
  return <div>复杂组件</div>;
}

最后,我们来看一张表,来对比一下类组件的生命周期函数和函数组件的Hooks

类组件生命周期钩子

函数组件 Hooks 实现方式

说明

constructor

直接在函数组件中初始化变量 + useState

设置初始状态和变量

componentDidMount

useEffect(() => {}, [])(空依赖)

组件挂载后执行一次

componentDidUpdate

useEffect(() => {}, [deps])(含依赖)

依赖变化时执行(模拟更新后操作)

componentWillUnmount

useEffect(() => { return () => {} }, [])

清理函数(组件卸载或依赖变化时执行)

shouldComponentUpdate

React.memo(针对 props 变化) + useMemo

控制是否重渲染(性能优化)

getDerivedStateFromProps

极少使用,可用 useEffect + 状态更新替代

根据 props 动态更新 state(谨慎使用)

tips:这里面试的时候我会问:你了解过react中的hooks吗?候选人如果回答了useEffect 的话我就会接着问:那你知道它的依赖数组中每一项都表示什么含义?如果依赖数组为空/只有一项的话会出现什么样的情况?

4、条件渲染与列表渲染(key 作用);

在react中,条件渲染指根据特定条件(如状态、属性)决定是否渲染组件或元素是否渲染,常见实现方式有以下几种:

1)、三元表达式

代码语言:javascript
复制
function UserStatus({ isLoggedIn }) {
  return (
    <div>
      {isLoggedIn ? (
        <p>欢迎回来!</p>
      ) : (
        <p>请先登录。</p>
      )}
    </div>
  );
}

2)、逻辑与 &&

代码语言:javascript
复制
function Notification({ hasUnread }) {
  return (
    <div>
      <h1>消息中心</h1>
      {hasUnread && <p>您有未读消息!</p>}
    </div>
  );
}
// 当 hasUnread 为 false 时,只会渲染 <h1>消息中心</h1>

3)、使用变量存储元素

代码语言:javascript
复制
function OrderStatus({ status }) {
  let statusElement;

  if (status === 'pending') {
    statusElement = <p>订单处理中...</p>;
  } else if (status === 'shipped') {
    statusElement = <p>订单已发货!</p>;
  } else if (status === 'delivered') {
    statusElement = <p>订单已送达!</p>;
  } else {
    statusElement = <p>订单状态未知</p>;
  }

  return <div>{statusElement}</div>;
}

4)、组件抽离

代码语言:javascript
复制
// 抽离条件组件
function LoggedInView() {
  return <p>欢迎回来!</p>;
}
function LoggedOutView() {
  return <p>请先登录。</p>;
}
// 父组件根据条件选择渲染
function UserStatus({ isLoggedIn }) {
  return <div>{isLoggedIn ? <LoggedInView /> : <LoggedOutView />}</div>;
}

列表渲染呢,它其实就是后端有时候会给我们一个json格式的对象,对象中有很多数组,每一个数组代表一项,我们前端需要做的就是把这些数据在页面中以各种形式呈现出来,例如卡片,表格等。

在react中,用的最多的就是map来遍历

代码语言:javascript
复制
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li>{todo.text}</li>
      ))}
    </ul>
  );
}
// 使用:<TodoList todos={[{ text: '学习React' }, { text: '写代码' }]} />

上面的代码其实是有问题的!你没有发现它都没有key吗?

key 是 React 用于识别列表中元素唯一性的特殊属性。

在map遍历中,key的核心作用有两个:

  • 优化重渲染性能:React 通过 

key 判断元素是否被添加、移除或重新排序,避免不必要的 DOM 操作。

  • 保持元素状态:当列表重新渲染时,相同 

key 的元素会保留其状态(如输入框内容、滚动位置)。

正确的代码应该是下面这个样子:

代码语言:javascript
复制
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}> {/* 使用唯一ID作为key */}
          {todo.text}
        </li>
      ))}
    </ul>
  );
}
// 数据示例:
// [
//   { id: 1, text: '学习React' },
//   { id: 2, text: '写代码' }
// ]

注意一下:key是唯一的!一般用id来作为key;key是稳定的!不能随意变化!key仅用于列表渲染,不会传递给子组件!

5、useState(状态更新规则)

useState是react中用于关于组件状态的一个核心hook,提供了声明和更新状态的能力。

useState 接收一个初始状态值(可以是任意类型:基本类型、对象、数组等),返回一个数组:

  • 第一个元素:当前状态值(如 count)。
  • 第二个元素:更新状态的函数(如 setCount),调用后会触发组件重新渲染。

看下面的代码:

代码语言:javascript
复制
function Example() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    console.log(count); // 输出旧值(0),因为更新是异步的
  };

  return <button onClick={handleClick}>点击</button>;
}

在这里需要注意一下:状态更新是替换而不是合并。

  • 对于对象状态setState 不会自动合并旧对象的属性,需要手动合并(与类组件的 this.setState 不同)。
  • 对于数组状态:同理,需要手动处理数组的添加、删除等操作,避免直接修改原数组(保持不可变性)。

对象状态:

代码语言:javascript
复制
function User() {
  const [user, setUser] = useState({ name: 'Alice', age: 20 });

  const updateAge = () => {
    // 错误:直接替换会丢失 name 属性
    // setUser({ age: 21 });

    // 正确:手动合并旧属性
    setUser({ ...user, age: 21 });
  };

  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
      <button onClick={updateAge}>增长年龄</button>
    </div>
  );
}

数组状态:

代码语言:javascript
复制
function TodoList() {
  const [todos, setTodos] = useState(['学习 React', '写代码']);

  const addTodo = () => {
    // 错误:直接修改原数组(违反不可变性)
    // todos.push('新任务');
    // setTodos(todos); // 不会触发更新(引用未变)

    // 正确:创建新数组(保持不可变性)
    setTodos([...todos, '新任务']);
  };

  return (
    <div>
      <ul>
        {todos.map((todo, index) => (
          <li key={index}>{todo}</li>
        ))}
      </ul>
      <button onClick={addTodo}>添加任务</button>
    </div>
  );
}

当新状态依赖于前一次状态时(如累加、基于旧数组添加元素),必须使用函数形式的更新函数,确保获取到最新的状态。

代码语言:javascript
复制
function Counter() {
  const [count, setCount] = useState(0);

  const incrementTwice = () => {
    // 错误:两次更新都基于初始值 0,最终结果为 1(而非 2)
    // setCount(count + 1);
    // setCount(count + 1);

    // 正确:函数形式接收前一次状态,确保更新准确
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1); // 最终结果为 2
  };

  return <button onClick={incrementTwice}>连续加 2</button>;
}

因为函数形式的更新会被加入队列,依次基于上一次的结果计算,避免因异步更新导致的状态覆盖。

6、useEffect(依赖项 + 清除副作用)

useEffect 是 React 中用于处理副作用的核心 Hook,它允许函数组件执行数据请求、事件监听、订阅等具有副作用的操作。

它接收两个参数。

  • 第一个参数:副作用函数(包含需要执行的副作用逻辑)。
  • 第二个参数:依赖项数组(控制副作用函数何时执行)。
代码语言:javascript
复制
import { useEffect } from 'react';
function Example() {
  // 副作用函数:组件挂载后执行,依赖项变化时重新执行
  useEffect(() => {
    // 副作用逻辑(如数据请求、事件监听)
    console.log('执行副作用');

    // 清除函数(可选):组件卸载或依赖变化时执行
    return () => {
      console.log('清除副作用');
    };
  }, [dependencies]); // 依赖项数组

  return <div>示例组件</div>;
}

接下来,我们重点来看看依赖项数组的作用与规则

1)、如果数组为空

则仅在组件首次挂载后执行一次,相当于类组件的 componentDidMount

适用于一次性副作用(如初始化数据请求、注册全局事件)。

代码语言:javascript
复制
function UserList() {
  const [users, setUsers] = useState([]);

  // 仅挂载时请求一次数据
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => setUsers(data));
  }, []); // 空依赖:只执行一次

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

2)、包含依赖项 [dep1, dep2, ...]

  • 执行时机:组件挂载后执行一次,且

当依赖项中的任意一个发生变化时重新执行,相当于类组件的 componentDidMount + componentDidUpdate

  • 适用场景:依赖特定变量的副作用(如根据 

id 重新请求数据、监听 props 变化)。

代码语言:javascript
复制
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  // 当 userId 变化时,重新请求用户数据
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data));
  }, [userId]); // 依赖 userId:userId 变化时执行

  return user ? <div>{user.name}</div> : <p>加载中...</p>;
}

3)、无依赖项(省略第二个参数)

  • 执行时机:

每次组件渲染后都执行(包括首次挂载和每次更新)。

  • 风险:可能导致无限循环(如副作用中修改了状态,触发重新渲染,再次执行副作用),

谨慎使用

代码语言:javascript
复制
// 不推荐:每次渲染都执行,可能引发性能问题
useEffect(() => {
  console.log('每次渲染都执行');
});

使用依赖项的时候,我们需要注意下面的这些问题:

第一、必须包含所有用到的变量:副作用函数中引用的所有组件内变量(如 state、props、函数)都必须加入依赖项数组,否则可能捕获旧值(闭包陷阱)。

代码语言:javascript
复制
function Example({ id }) {
  const [count, setCount] = useState(0);

  // 错误:依赖了 id 和 count,但未加入依赖数组
  useEffect(() => {
    console.log(`id: ${id}, count: ${count}`);
  }, []); // 空依赖会导致始终读取初始的 id 和 count

  // 正确:包含所有依赖
  useEffect(() => {
    console.log(`id: ${id}, count: ${count}`);
  }, [id, count]); // 依赖变化时更新
}

第二、依赖项比较是浅层的:React 通过浅层比较(Object.is)判断依赖是否变化,对于对象 / 数组,需确保引用变化时才更新(避免不必要的执行)。

代码语言:javascript
复制
function Example() {
  // 每次渲染都会创建新对象,导致副作用每次都执行(不推荐)
  const config = { timeout: 1000 };

  useEffect(() => {
    const timer = setTimeout(() => {}, config.timeout);
    return () => clearTimeout(timer);
  }, [config]); // 每次渲染 config 引用不同,副作用频繁执行

  // 优化:使用稳定的依赖
  const timeout = 1000; // 基本类型,引用稳定
  useEffect(() => {
    const timer = setTimeout(() => {}, timeout);
    return () => clearTimeout(timer);
  }, [timeout]); // 仅 timeout 变化时执行
}

在这里,我们顺便提一下使用useEffect来清理副作用的函数。

副作用函数可以返回一个清理函数,用于在组件卸载或依赖项变化时清除副作用,避免内存泄漏(如未移除的事件监听、未取消的定时器)。

执行时机一般是组件卸载前。

常见的使用场景有以下几个:

1)、移除事件监听

代码语言:javascript
复制
function WindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    // 定义事件处理函数
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    // 监听窗口大小变化
    window.addEventListener('resize', handleResize);
    handleResize(); // 初始化

    // 清理函数:移除事件监听
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空依赖:仅挂载时监听一次

  return <p>窗口大小:{size.width} × {size.height}</p>;
}

2)、清除定时器

代码语言:javascript
复制
function Countdown() {
  const [count, setCount] = useState(10);

  useEffect(() => {
    const timer = setInterval(() => {
      setCount(prev => prev - 1);
    }, 1000);

    // 清理函数:清除定时器
    return () => {
      clearInterval(timer);
    };
  }, []); // 空依赖:仅创建一次定时器

  return <p>倒计时:{count}</p>;
}

3)、取消网络请求:

代码语言:javascript
复制
function DataFetcher({ url }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 创建控制器用于取消请求
    const controller = new AbortController();
    const signal = controller.signal;

    fetch(url, { signal })
      .then(res => res.json())
      .then(data => setData(data))
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error('请求失败:', err);
        }
      });

    // 清理函数:取消请求(避免组件卸载后请求仍在进行)
    return () => {
      controller.abort(); // 取消请求
    };
  }, [url]); // 依赖 url:url 变化时重新请求

  return data ? <pre>{JSON.stringify(data)}</pre> : <p>加载中...</p>;
}

如果大家用react比较多的话,有时候你会发现如果依赖项数组设置的不合理,很容易造成无限循环,大幅降低性能。

主要就是因为副作用中修改了依赖项数组中的状态,导致依赖变化,触发副作用重新执行,形成循环。

那我们解决这类问题的时候就需要考虑以下两点:

  • 确保依赖项数组中只包含必要的变量。
  • 若需修改状态,使用函数形式的更新(避免依赖当前状态)。

看下面的代码:

代码语言:javascript
复制
function BadExample() {
  const [count, setCount] = useState(0);

  // 错误:依赖 count,且在副作用中修改 count,导致无限循环
  useEffect(() => {
    setCount(count + 1);
  }, [count]); // count 变化 → 副作用执行 → count 再变化 → ...

  return <p>{count}</p>;
}

还有一种情况就是:明明我们的数据更新了,但是页面上的内容还是原来的值,这种情况下就是因为它通过闭包读到了旧的值,导致执行时使用的不是最新值。

我们的解决办法当然是让它读取最新的值,我们可以结合 useRef 存储最新值。举个例子:

代码语言:javascript
复制
function ClosureExample() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  // 用 ref 实时记录最新值
  useEffect(() => {
    latestCount.current = count;
  }, [count]);

  useEffect(() => {
    const timer = setInterval(() => {
      // 正确:通过 ref 读取最新值
      console.log('最新 count:', latestCount.current);
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 空依赖:仅执行一次

  return <button onClick={() => setCount(c => c + 1)}>加 1</button>;
}

7、useRef(DOM 引用 + 保存持久值)

useRef 是 React 中一个灵活的 Hook,主要有两大两个核心用途:获取 DOM 元素引用 和 保存跨渲染周期的持久值。它不同于 useState,更新 useRef 不会触发发组件重新渲染,这使得它在处理某些特殊场景时非常有用。

基础用法:

useRef 接收一个初始值作为参数,返回一个可变 ref 对象,该对象有一个 current 属性,用于存储实际的值。

代码语言:javascript
复制
import { useRef } from 'react';
function Example() {
  // 创建一个 ref 对象,初始值为 null
  const myRef = useRef(null);

  // 可以通过 myRef.current 访问或修改值
  myRef.current = "Hello";
  console.log(myRef.current); // 输出 "Hello"

  return <div></div>;
}
  • ref 对象在组件的整个生命周期中保持不变(不会因重新渲染而重置)。
  • 修改 myRef.current 不会触发组件重新渲染(与 setState 不同)。

useRef 最常见的用途是获取 DOM 元素的直接引用,以便执行 DOM 操作(如聚焦输入框、获取元素尺寸等)。

1)、通过 ref 属性将 DOM 元素与 useRef 创建的对象关联:

代码语言:javascript
复制
function InputFocus() {
  // 创建 ref 用于关联 input 元素
  const inputRef = useRef(null);

  const handleFocus = () => {
    // 通过 current 获取 DOM 元素,调用 focus() 方法
    inputRef.current.focus();
  };

  return (
    <div>
      {/* 将 ref 关联到 input 元素 */}
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>聚焦输入框</button>
    </div>
  );
}

2)、通过 ref 获取 DOM 元素后,可以读取其尺寸、位置等信息:

代码语言:javascript
复制
function ElementSize() {
  const divRef = useRef(null);

  const logSize = () => {
    // 获取元素的 DOM 信息
    const rect = divRef.current.getBoundingClientRect();
    console.log('宽度:', rect.width);
    console.log('高度:', rect.height);
  };

  return (
    <div>
      <div 
        ref={divRef} 
        style={{ width: '200px', height: '100px', background: 'lightblue' }}
      >
        测试元素
      </div>
      <button onClick={logSize}>获取尺寸</button>
    </div>
  );
}

除了上面的两种用法之外,useRef它还可以保存一些跨组件渲染周期的变量。

useRef 可以保存跨组件渲染周期的变量,且修改这些变量不会触发重新渲染。这在需要跟踪状态但不需要触发 UI 更新的场景中非常有用。

例如:在类组件中,可通过 this.xxx 存储实例属性;在函数组件中,useRef 可实现类似功能

代码语言:javascript
复制
function Timer() {
  const [count, setCount] = useState(0);
  // 用 ref 存储定时器 ID(跨渲染保存)
  const timerRef = useRef(null);

  const startTimer = () => {
    // 避免重复创建定时器
    if (!timerRef.current) {
      timerRef.current = setInterval(() => {
        setCount(c => c + 1);
      }, 1000);
    }
  };

  const stopTimer = () => {
    // 清除定时器(使用 ref 中保存的 ID)
    clearInterval(timerRef.current);
    timerRef.current = null;
  };

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={startTimer}>开始</button>
      <button onClick={stopTimer}>停止</button>
    </div>
  );
}

同时,它还可以解决闭包的陷阱

在异步操作(如定时器、事件监听)中,函数组件的闭包可能捕获旧的状态值。使用 useRef 可以实时保存最新状态:

代码语言:javascript
复制
function ClosureFix() {
  const [count, setCount] = useState(0);
  // 用 ref 实时同步最新的 count 值
  const countRef = useRef(count);

  // 状态更新时,同步到 ref
  useEffect(() => {
    countRef.current = count;
  }, [count]); // 依赖 count,确保同步最新值

  const logLatestCount = () => {
    setTimeout(() => {
      // 正确获取最新 count(通过 ref)
      console.log('最新 count:', countRef.current);
    }, 1000);
  };

  return (
    <div>
      <p>当前 count:{count}</p>
      <button onClick={() => setCount(c => c + 1)}>加 1</button>
      <button onClick={logLatestCount}>1秒后打印最新值</button>
    </div>
  );
}

有时候,我们如果有一些临时数据,也可以放在useRef中来存储。例如存储请求的加载状态、临时计算结果等,避免因状态更新导致的不必要渲染:

代码语言:javascript
复制
function DataFetcher() {
  const [data, setData] = useState(null);
  // 用 ref 存储加载状态(不需要触发 UI 更新)
  const isLoadingRef = useRef(false);

  const fetchData = async () => {
    if (isLoadingRef.current) return; // 避免重复请求
    isLoadingRef.current = true;

    try {
      const res = await fetch('/api/data');
      const data = await res.json();
      setData(data); // 仅数据更新时触发渲染
    } finally {
      isLoadingRef.current = false;
    }
  };

  return (
    <div>
      <button onClick={fetchData}>获取数据</button>
      {data && <pre>{JSON.stringify(data)}</pre>}
    </div>
  );
}

最后,我们来比较一下useRef 与 useState

特性

useRef

useState

触发渲染

修改 current 不会触发渲染

调用更新函数会触发渲染

数据持久性

跨渲染周期保持不变

跨渲染周期保持不变

用途

获取 DOM 引用、保存非渲染数据

管理组件状态,驱动 UI 变化

访问方式

通过 ref.current 访问 / 修改

直接访问状态变量,通过更新函数修改

好啦,由于篇幅原因,本期文章就到这里,下一期我们继续来分享react相关的面试题,下期再见~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-29,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 1. 挂载阶段(组件创建并插入 DOM)
  • 2. 更新阶段(组件重新渲染)
  • 3. 卸载阶段(组件从 DOM 中移除)
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档