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

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

作者头像
爱学英语的程序媛
发布2026-09-15 11:17:06
发布2026-09-15 11:17:06
20
举报

哈喽小伙伴们,我们今天继续来接着上一期文章继续来分享 React

相关的面试题。废话不多说,直接上正文!

8、常用 Hooks(useContext/useReducer)及自定义 Hooks 编写;

原理与优化

在 React 中,除了上一篇文章中讲到的 useStateuseEffectuseRef 等基础 Hook 之外,useContext 和 useReducer 也是高频使用的 Hooks,它们分别用于跨组件状态共享和复杂状态逻辑管理。接下来我们一起来看看这两个hooks究竟是干啥的?

useContext 主要用于在组件树中跨层级传递数据,避免 props 逐层传递的繁琐。下面来看一段代码

代码语言:javascript
复制
// 创建上下文
const ThemeContext = React.createContext();
// 父组件提供值
function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Navbar />
      <Content />
    </ThemeContext.Provider>
  );
}
// 子组件消费上下文
function Navbar() {
  const { theme } = useContext(ThemeContext);
  return <div className={theme}>Navbar</div>;
}

在上面的代码中,Context 的本质是一个全局变量存储箱,Provider 组件通过 value 属性向下传递数据,而useContext 会订阅 Context 的变化,当 value 改变时会触发DOM的重新渲染。

而useReducer主要用于管理复杂组件状态,类似 Redux 的简化版。

举个例子:

代码语言:javascript
复制
// 定义 reducer 函数
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.payload, done: false }];
    case 'TOGGLE_TODO':
      return state.map(todo => 
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo
      );
    default:
      return state;
  }
}
// 组件中使用
function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []);

  return (
    <div>
      <button onClick={() => dispatch({ type: 'ADD_TODO', payload: 'New task' })}>
        Add
      </button>
      {todos.map(todo => (
        <div 
          key={todo.id}
          onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
          style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
        >
          {todo.text}
        </div>
      ))}
    </div>
  );
}

上面的代码基于 Redux 思想,通过纯函数(reducer)处理状态的更新,每次 dispatch 会触发 reducer 执行并返回新状态。相比 useState,它更适合处理多状态联动的复杂逻辑。

那接下来面试官可能就会问你,如果让你封装一个hooks,你会怎么封装,会考虑哪些因素呢?

那这里就提到了一个自定义的hooks.

我们封装的目的其实就是为了实现逻辑的复用,使我们组件内部的代码更加简洁。

我们也看到了很多react已经提供给我们的hooks,通过学习和使用他们,我们大概可以总结出下面的三条基本原则:

1)、以use打头

2)、是纯函数,不能依赖外部的状态

3)、内部可以调用其他 Hooks

下面,以开发中最常见的获取后端数据为例来展示一下我们是怎么封装一个自定义的hooks的?

代码语言:javascript
复制
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error('请求失败');
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        setError(err.message);
        setData(null);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]); // 仅当 url 变化时重新请求

  return { data, loading, error };
}

通过上面的代码大家应该能很清楚的看到具体的一些细节。其实,我们作为一名程序员,实现功能只是其中最基础的一步,我们更看重的是代码的严谨性。为了避免在运行的过程中出现一些奇怪的问题,接下来的这几个方面我们是不是应该也要考虑一下:

1)、有没有不必要的渲染?

  • 使用 React.memo 包装纯组件
  • 使用 useCallback 缓存函数引用
  • 使用 useMemo 缓存计算结果

  1. 2)、不合理的闭包是否会导致内存泄漏?
    • 确保依赖数组完整(比如ESLint 规则:exhaustive-deps)
    • 复杂场景可以使用 ref 存储最新值
  2. 3)、状态拆分是否合理?
    • 将不相关的状态拆分为多个 useState
    • 大型应用考虑使用状态管理库(Redux、Zustand 等)
  3. 4)、有没有不合理的请求?
    • 是否提供取消请求的能力?(如 AbortController)
    • 增加缓存机制减少重复请求
    • 支持手动触发更新吗?
    • 上面提到的问题其实解决方法有很多,这也反映了你对基础知识的一个掌握情况,如果在面试的时候你能举一反三,将理论知识结合到项目中具体的场景中来回答,那么将会给面试官留下一个很不错的印象。

9、React Fiber 架构(核心思想)

这里大家了解一下就行,不需要过度深入。

React Fiber 是 React 16 引入的全新协调(Reconciliation)引擎,其核心目标是解决大型应用中 UI 更新的性能问题,特别是解决长任务阻塞主线程导致的界面卡顿。

  1. 核心思想就这几个字:可中断,可恢复,优先级调度。
  2. 1)、将渲染工作分解为小单元
    • Fiber 把整个渲染过程(从虚拟 DOM 比对到实际 DOM 更新)拆分成一个个独立的 "工作单元"(每个 Fiber 节点对应一个工作单元)
    • 每个工作单元只负责处理一个组件的更新,完成后可以暂停、恢复或中断
  3. 2)、优先级调度机制
    • 为不同类型的更新分配优先级(如用户输入 > 动画 > 数据请求)
    • 高优先级任务可以打断低优先级任务,执行完成后再恢复低优先级任务
    • 优先级划分: Immediate:同步执行,最高优先级
    • UserBlocking:用户交互(如点击、输入),25ms 内完成
    • Animation:动画效果,16ms 内完成
    • Normal:普通优先级,无需紧急处理
    • Low:低优先级,可延迟
    • Idle:空闲时才执行
  4. 3)、增量渲染(Incremental Rendering)
    • 不再一次性完成所有渲染工作,而是分批处理
    • 每处理完一个工作单元,检查是否有更高优先级任务
    • 如果有则暂停当前工作,让出主线程,避免阻塞用户交互

它的工作原理主要就是双缓存和两阶段的处理。

首先是双缓存:

  1. 依赖于Fiber 的数据结构,每个 Fiber 节点对应一个组件,包含:
    • 组件类型、DOM 信息、指针(指向父、子、兄弟 Fiber)
    • 工作单元的状态(未开始、处理中、已完成)
    • 优先级标识和中断恢复信息

在这里存在两个 Fiber 树:分别是Current Tree和WorkInProgress Tree

  • Current Tree表示当前渲染在屏幕上的 Fiber 树
  • WorkInProgress Tree则表示正在构建的新 Fiber 树

工作完成后通过切换根节点指针实现高效更新,避免直接操作 DOM 树

其次是两个阶段,分别是调度阶段和提交阶段。

  • 在调度阶段,程序会找出前后虚拟 DOM 的差异(Diff 算法)
  • 这个过程可以中断,也可以被高优先级的任务打断
  • 最后,执行结束后会生成 "副作用"(Side Effects)列表

在提交阶段,

  • 它会根据副作用列表执行实际 DOM 操作
  • 这个过程是同步执行的,不可中断
  • 随后会触发 componentDidMount/Update 等生命周期

那React为什么要引入Fiber 架构呢?到底解决了什么问题?

我先说答案:

1)、避免阻塞主线程

2)、优化用户体验

3)、边界更加明显

接下来一个一个解释。

  • 关于第一条,我们的浏览器主线程需要同时负责 JS 执行、布局、绘制等
  • 如果出现一些长任务的话(如复杂组件渲染)就会导致界面卡顿
  • 在这里Fiber 就会通过时间切片(requestIdleCallback 模拟)让出主线程,提高任务处理的效率。

    • 关于第二条,我们在设计方案的时候有一个原则,要确保高优先级任务(如输入反馈)优先执行
    • 有时候,为了避免动画掉帧,需要时刻保持 60fps 流畅度
  1. 关于第三条,举个例子,
    • 某个组件出错不能导致整个应用崩溃,在业务层要打印详细的日志,便于局部错误捕获和恢复。

10、性能优化方案

在react中,性能优化其实有很多种方法,但有些是通用的,不管是vue还是react均可,在这里,我们先来看看组件层面的性能优化。

这里涉及到三个hooks,分别是memo、useMemo和useCallback

React.memo主要用于缓存组件渲染的结果。

代码语言:javascript
复制
// 避免子组件不必要的重渲染
const ChildComponent = React.memo(({ name, onClick }) => {
  console.log('ChildComponent 渲染了');
  return <button onClick={onClick}>{name}</button>;
});
// 使用方式
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("按钮");

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>计数: {count}</button>
      <ChildComponent name={name} onClick={() => {}} />
    </div>
  );
}

也就是说当父组件重渲染时,如果传给子组件的 props 没有变化,React.memo 会阻止子组件重渲染。

而useCallback主要用于缓存函数的引用。

代码语言:javascript
复制
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("按钮");

  // 缓存函数,避免每次渲染创建新函数
  const handleClick = useCallback(() => {
    console.log("点击了", name);
  }, [name]); // 依赖项变化时才重新创建函数

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>计数: {count}</button>
      <ChildComponent name={name} onClick={handleClick} />
    </div>
  );
}

useMemo主要用来缓存函数计算的结果。

代码语言:javascript
复制
function ExpensiveComponent({ list }) {
  // 缓存计算结果,避免每次渲染重新计算
  const sortedList = useMemo(() => {
    console.log("执行了排序操作");
    return [...list].sort((a, b) => b.value - a.value);
  }, [list]); // 仅当 list 变化时重新计算

  return (
    <ul>
      {sortedList.map(item => (
        <li key={item.id}>{item.name} - {item.value}</li>
      ))}
    </ul>
  );
}

接下来,我们来看看其他的两种性能优化的方法:列表懒加载和图片优化。

一般情况下,但我们的列表数据量大时,一次性渲染所有项会严重影响性能,可采用懒加载策略:

代码语言:javascript
复制
function LazyLoadList({ items }) {
  const [visibleCount, setVisibleCount] = useState(10);
  const observerTarget = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(entries => {
      // 当观察目标可见时,加载更多
      if (entries[0].isIntersecting && visibleCount < items.length) {
        setVisibleCount(prev => Math.min(prev + 10, items.length));
      }
    });

    if (observerTarget.current) {
      observer.observe(observerTarget.current);
    }

    return () => observer.disconnect();
  }, [visibleCount, items.length]);

  return (
    <div>
      {items.slice(0, visibleCount).map(item => (
        <div key={item.id} className="list-item">
          {item.content}
        </div>
      ))}

      {/* 观察点 - 用于触发加载更多 */}
      {visibleCount < items.length && (
        <div ref={observerTarget} className="loading-indicator">
          加载中...
        </div>
      )}
    </div>
  );
}

也可以使用现成的库如 react-window 或 react-virtualized 实现虚拟列表,只渲染可视区域内的项。

之前面试一位实习生同学的时候,让他介绍自己的项目亮点的时候,他说他对react的列表又封装了一层,实现了虚拟列表的功能,来进行性能优化。我问他为什么要封装,其实答案就在上面。

关于图片优化,其实有很多种方法。

在本期文章中,我们举几个例子。

1)、懒加载

代码语言:javascript
复制
function OptimizedImage({ src, alt, width, height }) {
  return (
    <img
      src={`${src}?w=${width}`} // 假设服务端支持按宽度裁剪
      alt={alt}
      loading="lazy" // 原生懒加载
      width={width}
      height={height}
      style={{ aspectRatio: width / height }} // 避免布局偏移
    />
  );
}

2)、使用webP格式

代码语言:javascript
复制
function ModernImage({ src, alt }) {
  return (
    <picture>
      {/* 优先使用 WebP 格式(更小的体积) */}
      <source srcSet={`${src}.webp`} type="image/webp" />
      {/* 降级方案 */}
      <img src={`${src}.jpg`} alt={alt} loading="lazy" />
    </picture>
  );
}

3)、结合具体的场景

例如在首页需要展示大量图片的情况下,我们可以使用一些清晰度不是特别高的图片,当用户点进去查看详情的时候再展示高分辨率的图片。

拓展:

11、React Router(路由跳转 + 参数传递)。

React Router 是 React 生态中处理路由的核心库,用于实现单页应用(SPA)的页面跳转和路由管理。接下来,我们一起来看看它是怎么管理路由的?

基础配置:

代码语言:javascript
复制
// src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import User from './pages/User';
import NotFound from './pages/NotFound';
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/user" element={<User />} />
        <Route path="*" element={<NotFound />} /> {/* 404 页面 */}
      </Routes>
    </BrowserRouter>
  );
}

路由跳转方式分为两种:声明式和编程式

声明式:

代码语言:javascript
复制
import { Link, NavLink } from 'react-router-dom';
function Navbar() {
  return (
    <nav>
      {/* 基础链接 */}
      <Link to="/">首页</Link>

      {/* 带样式的链接(激活时会添加 active 类) */}
      <NavLink to="/about" className={({ isActive }) => isActive ? 'active' : ''}>
        关于我们
      </NavLink>

      {/* 带状态的跳转 */}
      <Link 
        to="/user" 
        state={{ from: 'navbar', message: '来自导航栏的访问' }}
      >
        用户中心
      </Link>
    </nav>
  );
}

编程式:

代码语言:javascript
复制
import { useNavigate } from 'react-router-dom';
function LoginButton() {
  const navigate = useNavigate();

  const handleLogin = () => {
    // 模拟登录成功
    const isAuthenticated = true;

    if (isAuthenticated) {
      // 跳转到首页
      navigate('/');

      // 后退一页(类似 history.back())
      // navigate(-1);

      // 前进一页
      // navigate(1);

      // 带状态的跳转
      navigate('/success', { 
        state: { message: '登录成功!' },
        replace: true // 替换历史记录,避免回退到登录页
      });
    }
  };

  return <button onClick={handleLogin}>登录</button>;
}

参数传递方式也有两种:通过url路径参数和params查询参数:

通过url路径参数:适用于标识资源的唯一标识符(如用户 ID、文章 ID 等):

首先在路由中配置参数:

代码语言:javascript
复制
// 路由配置
<Route path="/user/:userId" element={<UserProfile />} />
<RouttId/comments/:commentId" element={<CommentDetail />} />
e path="/posts/:pos

然后在组件中获取参数

代码语言:javascript
复制
import { useParams } from 'react-router-dom';
function UserProfile() {
  // 获取路径参数
  const { userId } = useParams();

  return <div>用户 ID: {userId}</div>;
}
function CommentDetail() {
  // 多参数获取
  const { postId, commentId } = useParams();

  return (
    <div>
      文章 ID: {postId}
      <br />
      评论 ID: {commentId}
    </div>
  );
}

params查询参数:适用于筛选、分页等非唯一标识的参数:

我们可以在跳转时传递查询参数

代码语言:javascript
复制
// 声明式
<Link to="/products?category=electronics&sort=price">
  电子产品
</Link>
// 编程式
navigate('/products?category=electronics&sort=price');
// 更规范的方式(使用 URLSearchParams)
const params = new URLSearchParams();
params.append('category', 'electronics');
params.append('sort', 'price');
navigate(`/products?${params.toString()}`);

在组件中获取查询参数:

代码语言:javascript
复制
import { useSearchParams } from 'react-router-dom';
function ProductList() {
  // 类似 useState,返回 [查询参数对象, 修改函数]
  const [searchParams, setSearchParams] = useSearchParams();

  // 获取参数
  const category = searchParams.get('category');
  const sort = searchParams.get('sort');

  // 修改参数(会触发重新渲染)
  const handleSortChange = (newSort) => {
    searchParams.set('sort', newSort);
    setSearchParams(searchParams);
  };

  return (
    <div>
      <p>当前分类: {category}</p>
      <p>排序方式: {sort}</p>
      <button onClick={() => handleSortChange('newest')}>
        按最新排序
      </button>
    </div>
  );
}

状态参数state: 适用于传递不需要显示在 URL 中的数据(如临时消息、表单状态等)

跳转时传递状态:

代码语言:javascript
复制
// 声明式
<Link 
  to="/success" 
  state={{ message: '操作成功', from: 'settings' }}
>
  保存设置
</Link>
// 编程式
navigate('/success', {
  state: { message: '操作成功', from: 'settings' }
});

在目标组件中获取状态

代码语言:javascript
复制
import { useLocation } from 'react-router-dom';
function SuccessPage() {
  const location = useLocation();

  // 获取状态数据
  const { message, from } = location.state || {};

  return (
    <div>
      <h1>成功!</h1>
      <p>{message}</p>
      {from && <p>来自: {from}</p>}
    </div>
  );
}

嵌套路由:用于实现页面布局中的局部刷新(如侧边栏 + 内容区):

代码语言:javascript
复制
// 路由配置
<Route path="/dashboard" element={<DashboardLayout />}>
  <Route index element={<DashboardHome />} /> {/* 默认子路由 */}
  <Route path="profile" element={<UserProfile />} />
  <Route path="settings" element={<Settings />} />
</Route>
// 布局组件(DashboardLayout.jsx)
import { Outlet, Link } from 'react-router-dom';
function DashboardLayout() {
  return (
    <div className="dashboard">
      <aside>
        <Link to="/dashboard">首页</Link>
        <Link to="/dashboard/profile">个人资料</Link>
        <Link to="/dashboard/settings">设置</Link>
      </aside>
      <main>
        {/* 子路由内容会渲染在这里 */}
        <Outlet />
      </main>
    </div>
  );
}

讲了这么多,你记住了吗?

好啦,本期文章就到这里,面试题系列的文章也要接近尾声了,秋招正在如火如荼地进行,去年的这个时候我也非常焦虑,也曾日夜不停的参加笔试和面试,我深知这一路走来的艰辛,但我相信:坚持到底,就一定会成功!后面我也会分享自己作为业务部门的面试官筛选简历和面试候选人的一些经历以及真实的面试记录和面评,站在面试官的角度告诉大家怎么回答每一个问题,什么该答,什么不该答,通过每一道面试题考察的什么,为什么答得不是很标准,但最终还是给了候选人offer?

后面的文章将通过这些角度来给大家提供一些建议,大家敬请期待哦~

最后,感谢大家地阅读,我们下期再见~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-10-10,如有侵权请联系 cloudcommunity@tencent.com 删除
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档