
哈喽小伙伴们,我们今天继续来接着上一期文章继续来分享 React
相关的面试题。废话不多说,直接上正文!
8、常用 Hooks(useContext/useReducer)及自定义 Hooks 编写;
原理与优化
在 React 中,除了上一篇文章中讲到的 useState、useEffect、useRef 等基础 Hook 之外,useContext 和 useReducer 也是高频使用的 Hooks,它们分别用于跨组件状态共享和复杂状态逻辑管理。接下来我们一起来看看这两个hooks究竟是干啥的?
useContext 主要用于在组件树中跨层级传递数据,避免 props 逐层传递的繁琐。下面来看一段代码
// 创建上下文
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 的简化版。
举个例子:
// 定义 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的?
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)、有没有不必要的渲染?
9、React Fiber 架构(核心思想)
这里大家了解一下就行,不需要过度深入。
React Fiber 是 React 16 引入的全新协调(Reconciliation)引擎,其核心目标是解决大型应用中 UI 更新的性能问题,特别是解决长任务阻塞主线程导致的界面卡顿。
它的工作原理主要就是双缓存和两阶段的处理。
首先是双缓存:
在这里存在两个 Fiber 树:分别是Current Tree和WorkInProgress Tree
工作完成后通过切换根节点指针实现高效更新,避免直接操作 DOM 树
其次是两个阶段,分别是调度阶段和提交阶段。
在提交阶段,
那React为什么要引入Fiber 架构呢?到底解决了什么问题?
我先说答案:
1)、避免阻塞主线程
2)、优化用户体验
3)、边界更加明显
接下来一个一个解释。
10、性能优化方案
在react中,性能优化其实有很多种方法,但有些是通用的,不管是vue还是react均可,在这里,我们先来看看组件层面的性能优化。
这里涉及到三个hooks,分别是memo、useMemo和useCallback
React.memo主要用于缓存组件渲染的结果。
// 避免子组件不必要的重渲染
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主要用于缓存函数的引用。
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主要用来缓存函数计算的结果。
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>
);
}接下来,我们来看看其他的两种性能优化的方法:列表懒加载和图片优化。
一般情况下,但我们的列表数据量大时,一次性渲染所有项会严重影响性能,可采用懒加载策略:
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)、懒加载
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格式
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)的页面跳转和路由管理。接下来,我们一起来看看它是怎么管理路由的?
基础配置:
// 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>
);
}路由跳转方式分为两种:声明式和编程式
声明式:
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>
);
}编程式:
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 等):
首先在路由中配置参数:
// 路由配置
<Route path="/user/:userId" element={<UserProfile />} />
<RouttId/comments/:commentId" element={<CommentDetail />} />
e path="/posts/:pos然后在组件中获取参数
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查询参数:适用于筛选、分页等非唯一标识的参数:
我们可以在跳转时传递查询参数
// 声明式
<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()}`);在组件中获取查询参数:
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 中的数据(如临时消息、表单状态等)
跳转时传递状态:
// 声明式
<Link
to="/success"
state={{ message: '操作成功', from: 'settings' }}
>
保存设置
</Link>
// 编程式
navigate('/success', {
state: { message: '操作成功', from: 'settings' }
});在目标组件中获取状态
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>
);
}嵌套路由:用于实现页面布局中的局部刷新(如侧边栏 + 内容区):
// 路由配置
<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?
后面的文章将通过这些角度来给大家提供一些建议,大家敬请期待哦~
最后,感谢大家地阅读,我们下期再见~