1、什么是UI = f(state)
- 是react设计的核心原则,它将UI界面看成状态的函数映射,通过声明式的方式简化了UI开发,不需要像jQuery那样命令式的去操dom
- 状态是因,UI是果
- 状态变化,UI自动同步,无需手动操作dom
- 保证相同的状态输入,一定能够得到相同的UI界面
- 这种模式让代码更可预测,更易于维护
2、虚拟dom到底是什么
- 本质是一个js对象,是一个用来描述真实dom的轻量级副本
- 操作真实dom的性能开销往往是比较大的,react采用虚拟dom,在状态更新之前,会先通过diff算法比较新旧虚拟dom,找到最小差异,然后在统一的一次性更新为真实dom,尽可能的减少操作真实dom,来提高应用的性能
- 当然也不是什么时候,使用虚拟dom都能提高性能,如果只是简单的一次性操作dom,使用虚拟dom反而会增加性能开销,因为虚拟dom本身会创建对象,占用内存,diff算法的比较过程也需要一定的时间,但是在大型项目中,虚拟dom对性能的提升效果是显著的
- 所以虚拟dom一个“以空间换时间“的设计,用js的内存开销,换取真实dom的操作性能优化
3、vite相比于webpack快在哪
-
webpack的痛点:
-
开发时:
- 需要先递归解析所有模块依赖,构建出完整的依赖树,然后将所有模块打包成一个或者多个bundle.js才能启动开发服务器。
- 对于大型项目,这个过程往往比较耗时,几分钟甚至十几分钟,因为即使只是改一个文件,webpack也需要重新处理整个依赖链,效率比较低
-
依赖处理:
- webpack使用的是自己js解析器,速度会比较慢
-
热更新:
- webpack需要重新构建被修改的模块,以及依赖的模块组,甚至可能触发整个bundle重新生成,对于复杂项目,这个过程往往需要几百毫秒到几十秒,且容易出现“更新失效”,需要手动刷新页面
-
生产环境:采用自研打包引擎
-
-
vite解决方案:
-
开发时:
- vite利用浏览器原生支持的 ES module,完全跳过了打包步骤
- 按需编译,只处理当前用到的模块,启动速度非常快(毫秒级的)
-
依赖处理:
- vite使用的是Go语言编写的esbuild,速度比js的解析器快10-100倍
-
热更新:
- vite的热更新基于原生的ESM精确更新,只会重新编译被修改的模块,通过ESM的依赖图,vite能够精确通知浏览器只更新这个模块及其直接依赖
-
生产环境:采用基于Rollup二次封装的打包器,专注于按需打包
-
-
适用场景和优缺点:
-
webpack:
- 更适用于大型项目,需要精细化打包配置项目
- webpack的生态强大
- 兼容性更强
-
vite:
- 适用于中小型项目,不需要复杂打包的场景
- vite是新生的技术,生态相对没有那么强大
- 兼容性也没有那么好,对于旧版本的IE浏览器,需要配置额外插件@vitejs/plugin-legacy(通过 Babel 转译 + 自动注入 polyfill),会增加一定构建体积和构建耗时
-
4、函数组件和类组件的本质区别
-
函数组件:
- 编程范式:函数式编程,无实例,纯函数
- 状态管理:依赖hook函数suseState
- 生命周期:没有自己的声明周期,可以用useEffect钩子来模拟
- this处理:没有this,避免了复杂的this绑定问题
- 性能开销:轻量,无实例,优化简单(
React.memo) - 逻辑复用:自定义hooks,简洁,无嵌套
-
类组件:
- 编程范式:面向对象编程,有实例
- 状态管理:依赖this.state,存储在实例上
- 生命周期:有自己的生命周期方法
- this处理:有this,需要手动绑定this
- 性能开销:有实例开销,优化复杂(
shouldComponentUpdate) - 逻辑复用:高阶组件,可能会有产生嵌套地狱
5、react为什么强调props的不可变性
-
在React中,props的不可变性是React的核心设计原则之一,主要原因如下
-
为了保证组件的可预测性:
- 数据都是从父组件通过props传下来的,统一在父组件中维护,子组件只能读取,避免子组件和父组件同时修改数据,导致状态不一致,调试时难以追溯变化来源。
- 函数组件本质是纯函数(输入相同则输出一定相同),如果props可能被修改,那相同的输入就有可能得到不同的输出,组件会变得不可预测
-
方便配合
React.memo、shouldComponentUpdate来优化渲染性能:- 在React中,父组件重新渲染时,会创建全新的props,
React.memo、shouldComponentUpdate默认通过浅比较(比较引用是否相同)来判断props是否变化,从而前端子组件是否需要重新渲染 - 如果props的属性可以被改变,修改props的某个属性时,应用地址不会变化,使用
React.memo、shouldComponentUpdate来优化渲染就失去了意义。
- 在React中,父组件重新渲染时,会创建全新的props,
-
保证单向数据流的一致性:
- 数据只会从父组件传到子组件,如果子组件需要修改数据,也是通过父组件提供的修改方法,这样便于追踪变化,使得调试变得简单
-
6、useState的函数式更新有什么好处
-
确保使用最新的状态值
import { useState } from 'react'; export default function About() { const [count, setCount] = useState(0); const handleClick = ()=>{ //页面显示1,而非2 setCount(count + 1) setCount(count + 1) //页面显示预期结果2 setCount(count=>count + 1) setCount(count=>count + 1) } return ( <> <h1>the count value is {count}</h1> <h1 onClick={handleClick}>Hello About</h1> </> ); } -
避免闭包陷阱
import { useState } from 'react'; export default function About() { const [count, setCount] = useState(0); const handleClick = ()=>{ //页面一直显示1 setInterval(()=>{ setCount(count + 1) },1000) //页面显示1,2,3,4... setInterval(()=>{ setCount(count=> count + 1) },1000) } return ( <> <h1>the count value is {count}</h1> <h1 onClick={handleClick}>Hello About</h1> </> ); } -
简化复杂状态依赖的更新逻辑
function TodoList() { const [todos, setTodos] = useState([]); // 函数式更新:基于前一次的 todos 数组添加新项 const addTodo = (text) => { setTodos(prevTodos => [...prevTodos, { id: Date.now(), text }]); }; // 函数式更新:基于前一次的 todos 数组删除项 const deleteTodo = (id) => { setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id)); }; return <div>{/* ... */}</div>; }
7、如何优化useContext
useContext是React中用于跨组件层级共享数据的强大工具,但是如果使用不当,可能会造成组件的不必要重渲染,影响性能。
-
核心问题:为什么useContext可能导致性能问题
- 当
Context.Provider的value变化时,所有使用useContext(context)的组件都会强制重新渲染,无论他们是否依赖value中变化的部分。
- 当
-
优化方案:
-
按功能拆分
context,尽可能的精细化context的自责,减少单个context的影响范围 -
缓存
value:-
问题:每次 Provider 组件重渲染,
value都会被重新创建(引用变化),即使里面的状态没有实际变化,消费组件也会因为 “引用变化” 而触发重渲染。 -
解决:使用
useMemo缓存value,避免无效引用变化,阻止不必要的渲染 -
示例:
function UserProvider({ children }) { const [user, setUser] = useState(null); // 只有 user 或 setUser 变化时,value 才会重新创建(引用变化) const value = useMemo(() => ({ user, setUser }), [user, setUser]); return <UserContext.Provider value={value}>{children}</UserContext.Provider>; }
-
-
使用
React.memo:-
问题:即使
context拆得再好,消费组件还是可能会因为父组件的重新渲染而触发重新渲染 -
解决:可以使用
React.memo包裹消费组件,避免因为父组件重新渲染而自身不必要的重新渲染 -
示例:
// 只有当依赖的 Context 变化,或接收的 props 变化时,才会重渲染 const UserInfo = React.memo(() => { const { user } = useContext(UserContext); return <div>{user?.name}</div>; });
-
-
8、useState和useReducer
-
useState
-
可以用于任何类型的状态管理,但是通常用于简单类型的状态管理
-
语法
const [state, setState] = useState(initialState)-
initialState可以是任何类型
- 如果是函数类型,该函数只会在组件首次渲染时执行一次,避免每次重复计算,函数返回值为状态的初始值
-
-
-
useReducer
-
通常用于管理逻辑复杂状态
-
语法
const [state, dispatch] = useReducer(reducer, initialState, init?)reducer:(state,action)=>newState,一个纯函数,接收(state,action)两个参数,返回新状态,action描述更新的对象,通常包含type、payloadinitialState:状态的初始值,与useState类似init可选:惰性初始化函数,只在组件首次渲染时执行一次(通过计算得到状态的初始值,与useState的函数式类似)
-
9、useImperativeHandle和useFowardRef
import { forwardRef, useImperativeHandle, useRef } from 'react';
// 子组件:结合 forwardRef 和 useImperativeHandle
const CustomInput = forwardRef((props, ref) => {
// 子组件内部的 DOM ref
const inputRef = useRef(null);
// 自定义子组件通过 ref 暴露给父组件的方法
useImperativeHandle(ref, () => ({
// 暴露 focus 方法
focus: () => {
inputRef.current.focus();
},
// 暴露 clear 方法
clear: () => {
inputRef.current.value = '';
},
// 可以暴露属性
getValue: () => inputRef.current.value
}), []); // 依赖为空,只初始化一次
return <input {...props} ref={inputRef} />;
});
// 父组件:通过 ref 访问子组件暴露的自定义方法
function Parent() {
const customInputRef = useRef(null);
return (
<div>
<CustomInput ref={customInputRef} placeholder="请输入" />
<button onClick={() => customInputRef.current.focus()}>聚焦</button>
<button onClick={() => customInputRef.current.clear()}>清空</button>
<button onClick={() => console.log(customInputRef.current.getValue())}>
获取值
</button>
</div>
);
}
useFowardRef:使用useFowardRef包裹子组件,子组件可以接收两个参数,props和ref,ref是父组件使用useRef生成的ref容器,子组件接收到之后,可以直接绑定在dom上,这样父组件就可以借助传下来的ref,访问到子组件内部的真实domuseImperativeHandle:有的时候,我们不希望自己定义的组件,在被外部调用时随意调用组件内的dom方法,只给外部组件调用我们允许的方法,这个时候就可以使用useImperativeHandle来暴露一些我们允许的方法
10、react的并发特性
简单来说React 的并发特性是 “可中断的渲染” 和 “优先级调度” 的底层能力,是React 18引入的核心底层能力,它允许React中断、暂停、恢复甚至放弃渲染工作,从而优化应用在复杂场景下的响应性;其核心目的是:在处理耗时任务(如大量计算、复杂UI渲染)时,不阻塞用户交互(如点击、输入、滚动等),让应用保持流畅;
-
可中断的渲染:React的渲染过程分为Render 阶段和Commit 阶段,并发特性让Render 阶段变得可中断,当高优先级的任务(如用户输入)到来时,先处理高优先级的任务Render,完成后再决定是恢复处理低优先级的Render,还是直接丢弃(如果已过时)
- Render 阶段:计算组件树的新状态,生成新的虚拟dom(不操作真实dom)
- Commit 阶段:将虚拟dom的变化应用到真实dom(此阶段不可中断,因为涉及dom操作,必须原子化执行)
-
优先级调度:React 会为不同类型的任务分配优先级,确保高优先级任务(如用户交互)优先执行
11、开发者如何利用React的并发特性:useTransition和useDeferredValue
-
useTransition:用来标记低优先级更新import { useTransition, useState } from 'react'; function SearchBox() { const [input, setInput] = useState(''); const [list, setList] = useState([]); // isPending 表示过渡任务是否在进行中 const [isPending, startTransition] = useTransition(); const handleInput = (e) => { const value = e.target.value; // 1. 高优先级:立即更新输入框(不被阻塞) setInput(value); // 2. 低优先级:标记为过渡任务,可被中断 startTransition(() => { // 模拟耗时的列表筛选(如从大量数据中过滤) const filteredList = heavyFilter(value); setList(filteredList); }); }; return ( <div> <input value={input} onChange={handleInput} /> {isPending ? <p>加载中...</p> : ( <ul>{list.map(item => <li key={item}>{item}</li>)}</ul> )} </div> ); }语法:
const [isPending, startTransition] = useTransition();isPending:过渡任务是否在进行中startTransition(callBack):标记需要过渡的任务,callBack的执行可以被中断 -
useDeferredValue:为值创建延迟版本(类似防抖)import { useDeferredValue, useState } from 'react'; function SearchBox() { const [input, setInput] = useState(''); // 为 input 创建延迟版本,低优先级更新 const deferredInput = useDeferredValue(input); return ( <div> <input value={input} onChange={(e) => setInput(e.target.value)} /> {/* 依赖延迟值,低优先级渲染 */} <SearchResults query={deferredInput} /> </div> ); }
12、如何理解react组件的单一职责
-
核心思想:一个组件只负责一件事,当它需要变化时,只有一个原因;好的组件就像乐高积木,每一个组件都可以独立存在,又可以互相组合实现逻辑复用。
-
为什么需要单一职责:
- 提高组件复用性
- 降低维护成本
- 减少副作用和冲突
- 便于测试
13、容器组件和展示组件已经过时了吗
在React社区中,“容器组件”和“展示组件”的划分是早起非常流行的一种设计模式,但随着React的发展,尤其是hooks的出现,这种模式的严格边界正在被弱化,但它的核心思想(关注点分离) 并没有过时,而是以更灵活的形式延续。
为什么要区分容器组件和展示组件?核心目的是为了分离数据逻辑和UI渲染
- 容器组件:专注于数据逻辑处理,并且将数据传递给展示组件,不用关注UI展示的细节
- 展示组件:专注UI的展示细节处理,不用关心数据的处理逻辑,无副作用可复性高
hooks带来的改变:随着hooks的出现,函数组件也能拥有自己的状态和生命周期,我们可以很轻松的通过自定义一个hooks实现逻辑的复用,不用再使用容器组件来嵌套实现逻辑的复用,不用再严格的区分UI展示和状态处理组件,在一个函数组件中,返回UI的实现,通过hooks实现逻辑的复用。
14、react中的组合与继承:为什么推荐组合?
在React中,组合和继承是两种实现逻辑复用的方式
- 组合:核心是将组件视为零件,通过组合搭建负责功能,类似乐高积木(组合与 React 的 “声明式编程” 理念一致,并且具有灵活性、低耦合、清晰性等特点)
- 继承:核心是子类继承父类的属性和方法,通过类的继承关系,复用父组件的逻辑或者UI(容易造成组件组件耦合过重,难以维护)
15、react中的错误边界
在React中,错误边界(Error Boundary)是一种特殊组件,有利于捕获处理其子组件树中抛出的 js 错误,避免错误导致整个应用崩溃,同事提供有好的错误提示页面。
在React 16 之前,组件中抛出的错误可能导致整个应用崩溃,用户只能看见白屏,很不友好。
如何创建错误边界组件?
错误边界必须是类组件(目前函数组件还不支持错误边界,因为它依赖类组件的生命周期方法)需要实现以下两个生命周期方法中的至少一个:
-
static getDerivedStateFromError(error):- 用于更新状态,让组件在下一次渲染时显示备用 UI。
- 接收
error作为参数,返回一个对象(用于更新 state)。
-
componentDidCatch(error, errorInfo):- 用于记录错误信息(如发送到日志服务)。
- 接收错误对象
error和包含堆栈信息的errorInfo。
错误边界组件实例
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; // 初始状态:无错误 } // 捕获错误后更新状态,显示备用 UI static getDerivedStateFromError(error) { return { hasError: true }; // 将 hasError 设为 true } // 记录错误信息 componentDidCatch(error, errorInfo) { console.error("错误边界捕获到错误:", error, errorInfo); // 可在此处将错误发送到日志服务,如 Sentry } render() { // 如果有错误,显示备用 UI if (this.state.hasError) { return this.props.fallback || <h2>发生错误,请刷新页面重试。</h2>; } // 无错误时,渲染子组件树 return this.props.children; } }使用错误边界组件示例
function App() { return ( <div> <h1>我的应用</h1> {/* 包裹可能出错的组件 */} <ErrorBoundary fallback={<h2>用户列表加载失败</h2>}> <UserList /> {/* 可能抛出错误的组件 */} </ErrorBoundary> {/* 其他不受影响的组件 */} <Footer /> </div> ); }错误边界的限制:错误边界不能捕获以下场景的错误
- 自身的错误(只能捕获子组件树中的错误)
- 事件处理器中的错误(如
onClick、onChange等,React 希望开发者自行处理这些错误,如用try/catch) - 异步代码中的错误(如
setTimeout、fetch回调等,需自行用try/catch处理) - 服务端渲染期间的错误
17、怎么设计一个组件库
-
明确设计目标与边界:
- 使用场景:是内部业务组件(贴合特定产品),还是公共组件(类似antd、element)
- 组件范围:包含哪些组件,基础组件(按钮、文本输入框),业务组件(表单、表格)
- 技术栈:结合团队成员技术情况,以及新技术的成熟度、上手难度等,综合考虑技术选用
-
核心设计原则:
-
单一职责:每个组件只做一件事,避免过渡封装,复用更方便
-
API 设计一致性
- 命名规范:props命名统一,见名之意(
onChange处理变化、onClick处理点击、disabled处理禁用) - 行为一致:相似组件把持相同交互逻辑(
Select和Checkbox的value和onChange行为一致) - 避免冗余:避免重复功能的props(如同时提供
isDisabled和disabled)
- 命名规范:props命名统一,见名之意(
-
可定制与扩展性
- 样式定制:支持主题(颜色,字体)定制,通过css变量或者主题配置实现
- 行为扩展:允许通过props覆盖默认行为(如
renderXXX自定义渲染函数) - 插槽机制:通过
children或者具名插槽(如header、footer)支持灵活内容插入
-
可访问性(A11y):确保组件支持键盘导航、屏幕阅读器等辅助工具
-
受控组件优先,兼容非受控组件
-
-
扩展与维护:
- 文档与示例
- 国际化
- 支持按需引入组件和样式
- 社区反馈:搜集用户issues,持续迭代组件功能和维护组件
18、原子设计
原子设计是一种 “自下而上” 的组件构建方法论,通过将 UI 分解为原子、分子、有机体、模板和页面 5 个层级,实现组件的高度复用和 UI 一致性。在 React 中,这种思想与组件化理念完美契合,尤其适合大型应用或需要长期维护的项目。
核心价值在于:用最小的成本构建一致、灵活、可扩展的 UI 系统,让开发者从重复劳动中解放出来,专注于业务逻辑而非 UI 细节。
tailwind css是一个原子化工具类
19、如何实现一个受保护的路由
封装一个路由保护组件,检查是否有权限,有权限渲染路由元素或路由出口<Outlet />,没有权限渲染指定页面(使用<Navigate />来实现路由跳转)
// ProtectedRoute.jsx - 受保护路由组件
import { Navigate, Outlet } from 'react-router-dom';
// 权限验证逻辑
const ProtectedRoute = ({ element, requiresAuth, roles = [] }) => {
// 1. 获取用户登录状态和角色(实际项目中从Context/Redux获取)
const { isAuthenticated, userRole } = JSON.parse(
localStorage.getItem('user') || '{"isAuthenticated": false, "userRole": null}'
);
// 2. 未登录且需要权限时,重定向到登录页
if (requiresAuth && !isAuthenticated) {
return <Navigate to="/login" state={{ from: window.location.pathname }} replace />;
}
// 3. 已登录但角色不匹配时,重定向到无权限页
if (requiresAuth && roles.length > 0 && !roles.includes(userRole)) {
return <Navigate to="/unauthorized" replace />;
}
// 4. 权限通过,渲染对应组件
return element || <Outlet />;
};
export default ProtectedRoute;
路由配置表
// routes.js - 路由表配置
import Home from './pages/Home';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard'; // 需要登录
import AdminPanel from './pages/AdminPanel'; // 需要管理员权限
import NotFound from './pages/NotFound';
// 路由配置数组
const routes = [
{
path: '/',
element: <Home />,
requiresAuth: false, // 不需要登录
},
{
path: '/login',
element: <Login />,
requiresAuth: false,
},
{
path: '/dashboard',
element: <Dashboard />,
requiresAuth: true, // 需要登录
roles: [], // 空数组表示所有登录用户可访问
},
{
path: '/admin',
element: <AdminPanel />,
requiresAuth: true,
roles: ['admin'], // 仅管理员可访问
},
{
path: '*',
element: <NotFound />,
requiresAuth: false,
},
];
export default routes;
App组件
// App.jsx - 应用入口,渲染路由表
import { useRoutes, Navigate } from 'react-router-dom';
import routes from './routes';
import ProtectedRoute from './ProtectedRoute';
import Unauthorized from './pages/Unauthorized';
const App = () => {
// 转换路由配置为React Router可识别的格式
const renderedRoutes = routes.map((route) => ({
path: route.path,
element: (
<ProtectedRoute
requiresAuth={route.requiresAuth}
roles={route.roles}
>
{route.element}
</ProtectedRoute>
),
}));
// 额外添加无权限页面路由
renderedRoutes.push({
path: '/unauthorized',
element: <Unauthorized />,
});
// 使用useRoutes渲染路由表
return useRoutes([
...renderedRoutes,
// 根路径重定向
{ path: '/', element: <Navigate to="/home" replace /> }
]);
};
export default App;
20、redux-toolkit解决了哪些痛点
Redux Toolkit(简称 RTK)是 Redux 官方推出的工具集,旨在解决传统 Redux 开发中的一系列痛点,简化 Redux 代码编写流程,让开发者更专注于业务逻辑而非模板代码,主要解决一下痛点
-
消除”样板代码“冗余问题:统 Redux 开发需要手动编写action type 常量、action 创建函数、reducer 开关语句,代码冗长且重复
-
传统写法:
// 传统 Redux:大量样板代码 // action type const INCREMENT = 'counter/increment'; const DECREMENT = 'counter/decrement'; // action 创建函数 export const increment = (payload) => ({ type: INCREMENT, payload }); export const decrement = (payload) => ({ type: DECREMENT, payload }); // reducer const initialState = { value: 0 }; export default function counterReducer(state = initialState, action) { switch (action.type) { case INCREMENT: return { ...state, value: state.value + action.payload }; case DECREMENT: return { ...state, value: state.value - action.payload }; default: return state; } } -
RTK:
// Redux Toolkit:简洁代码 import { createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', // 自动生成 action type 的前缀(如 "counter/increment") initialState: { value: 0 }, reducers: { // 直接定义 reducer 逻辑,自动生成对应的 action 创建函数 increment: (state, action) => { state.value += action.payload; // 内部使用 Immer 库,支持“直接修改”状态(实际是 immutable 更新) }, decrement: (state, action) => { state.value -= action.payload; } } }); // 自动生成的 action 创建函数 export const { increment, decrement } = counterSlice.actions; // 自动生成的 reducer export default counterSlice.reducer;
-
-
简化 Immutable 状态更新:Redux 要求状态更新必须是不可变(immutable) 的,传统方式需要手动使用扩展运算符(
...)或Object.assign,嵌套结构时尤为繁琐易错-
传统写法:
// 传统 Redux:手动 immutable 更新 case 'user/updateAddress': return { ...state, user: { ...state.user, address: { ...state.user.address, city: action.payload.city // 嵌套层级越深,代码越复杂 } } }; -
RTK写法:
// 传统 Redux:手动 immutable 更新 case 'user/updateAddress': return { ...state, user: { ...state.user, address: { ...state.user.address, city: action.payload.city // 嵌套层级越深,代码越复杂 } } };
-
-
简化 Store 配置流程
-
传统写法:
// 传统 Redux:复杂的 store 配置 import { createStore, applyMiddleware, combineReducers } from 'redux'; import thunk from 'redux-thunk'; import { composeWithDevTools } from 'redux-devtools-extension'; import counterReducer from './counter'; import userReducer from './user'; const rootReducer = combineReducers({ counter: counterReducer, user: userReducer }); const store = createStore( rootReducer, composeWithDevTools(applyMiddleware(thunk)) // 手动集成中间件和 devTools ); -
RTK写法:
// Redux Toolkit:简化 store 配置 import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './counterSlice'; import userReducer from './userSlice'; export const store = configureStore({ reducer: { counter: counterReducer, user: userReducer // 自动合并 reducer,无需 combineReducers } // 自动集成 thunk、Redux DevTools,无需手动配置 });
-
-
简化异步逻辑处理:
-
传统
// 传统 Redux:手动处理异步 action const fetchUserStart = () => ({ type: 'user/fetchStart' }); const fetchUserSuccess = (data) => ({ type: 'user/fetchSuccess', payload: data }); const fetchUserFailure = (error) => ({ type: 'user/fetchFailure', payload: error }); // 异步 thunk action export const fetchUser = (userId) => { return async (dispatch) => { dispatch(fetchUserStart()); try { const response = await fetch(`/api/users/${userId}`); const data = await response.json(); dispatch(fetchUserSuccess(data)); } catch (error) { dispatch(fetchUserFailure(error.message)); } }; }; -
RTK
// Redux Toolkit:简化异步逻辑 import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; // 1. 自动生成异步 action(包含 pending/fulfilled/rejected 状态) export const fetchUser = createAsyncThunk( 'user/fetchUser', // action type 前缀 async (userId, { rejectWithValue }) => { try { const response = await fetch(`/api/users/${userId}`); return await response.json(); } catch (error) { return rejectWithValue(error.message); // 传递错误信息 } } ); // 2. 在 slice 中处理异步结果 const userSlice = createSlice({ name: 'user', initialState: { data: null, loading: false, error: null }, extraReducers: (builder) => { builder .addCase(fetchUser.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchUser.fulfilled, (state, action) => { state.loading = false; state.data = action.payload; }) .addCase(fetchUser.rejected, (state, action) => { state.loading = false; state.error = action.payload; }); } });
-
-
规范 Redux 最佳实践:
- 传统:传统 Redux 没有强制的代码组织规范,开发者可能将 action、reducer 分散在多个文件中,导致项目结构混乱。
- RTK:通过
createSlice鼓励 “切片(Slice) ” 模式:将一个功能模块的 action、reducer 集中在一个 slice 文件中,形成独立的功能单元,使代码结构更清晰、更易维护。
总结
- 消除样板代码(自动生成 action、reducer);
- 简化 immutable 状态更新(内置 Immer);
- 简化 store 配置(自动集成中间件和 devTools);
- 简化异步逻辑(
createAsyncThunk处理异步流程); - 规范代码组织(切片模式)。
21、immer.js为什么在redux生态中如此重要
Immer.js 在 Redux 生态中占据重要地位,核心原因是它解决了 Redux 开发中最棘手的问题之一 ——不可变状态更新的复杂性。Redux 要求状态必须是不可变的(Immutable),但手动处理不可变更新不仅繁琐易错,还会显著降低开发效率。Immer 通过 “以可变的方式编写不可变代码” 的创新思路,完美适配了 Redux 的设计理念,成为 Redux Toolkit 的核心依赖。
Redux 的核心设计原则之一是 “状态是只读的,只能通过 action 和 reducer 生成新状态” 。这意味着:
- 不能直接修改原状态(如
state.count = 1是禁止的); - 必须通过复制原状态并修改副本的方式更新(如
return { ...state, count: 1 })。
Immer 如何解决不可变更新的痛点?
Immer的核心是通过“代理 proxy”机制创建”草稿状态(Draft)“,允许开发者以直接修改的方式操作草稿,最后由 Immer 自动转化为不可变的状态。
总结
Immer.js 之所以在 Redux 生态中至关重要,是因为它解决了 Redux 开发中最实际的痛点 ——不可变状态更新的复杂性。通过允许开发者以 “可变的方式编写不可变代码”,Immer 大幅简化了 Redux reducer 的编写,降低了学习门槛,同时保证了状态的可预测性和调试体验。Redux Toolkit 将 Immer 作为核心依赖后,这种优势被进一步放大,使得现代 Redux 开发变得简洁高效。可以说,Immer 是 Redux 生态从 “繁琐” 走向 “易用” 的关键推动力之一。
22、定位react性能瓶颈的常用工具
-
React官方工具
-
React Developer Tools(浏览器扩展)
- Components 面板:查看组件层级、props、state,识别不必要的重渲染。
- Profiler 面板:记录组件渲染次数、耗时,标记 “昂贵渲染”(耗时超过阈值的组件)。
-
React Profiler API(代码级埋点)
import { Profiler } from 'react'; // 回调函数:记录性能数据 function onRenderCallback( id, // 标记 Profiler 的 id phase, // "mount" 或 "update" actualDuration, // 本次渲染实际耗时 baseDuration, // 预估不缓存时的耗时 startTime, // 渲染开始时间 commitTime, // 提交到 DOM 的时间 interactions // 关联的用户交互 ) { console.log(`组件 ${id} 渲染耗时: ${actualDuration}ms`); } // 包裹需要监控的组件树 function App() { return ( <Profiler id="UserList" onRender={onRenderCallback}> <UserList users={users} /> </Profiler> ); } -
适用场景:生产环境性能监控、特定组件的渲染耗时统计、与用户交互(如点击、输入)关联的性能分析
-
-
浏览器原生工具(F12)
-
Chrome DevTools(Performance 面板) :用于分析整个应用的运行时性能,包括 JavaScript 执行、DOM 渲染、布局(Layout)、绘制(Paint)等
-
使用步骤:
- 打开 Chrome DevTools → Performance 面板。
- 点击 “录制” 按钮,操作应用(如滚动、点击)。
- 停止录制,查看性能火焰图。
-
关注指标:
- Long Tasks:耗时超过 50ms 的任务,会阻塞主线程,导致卡顿。
- Rendering 部分:Layout(重排)和 Paint(重绘)的耗时,频繁的重排重绘是性能杀手。
- React 相关:火焰图中标记为
ReactDOM.render或updateComponent的调用栈,可定位渲染瓶颈。
-
优势:全面分析前端性能,不仅限于 React,能发现 JS 执行、DOM 操作等底层问题。
-
-
Chrome DevTools(Memory 面板) :用于检测内存泄漏(内存占用持续增长且不释放)
-
使用步骤:
- 打开 Memory 面板,选择 “Allocation Sampling”。
- 操作应用(如反复切换组件),点击 “Take snapshot” 记录内存快照。
- 对比多次快照,查看是否有组件实例、事件监听等未被正确回收。
-
React 常见内存泄漏场景:
- 未清除的定时器(
setInterval)或事件监听(window.scroll)。 - 闭包中意外保留的组件引用。
- 第三方库(如图表、地图)未正确销毁实例。
- 未清除的定时器(
-
-
-
第三方工具库
-
Lighthouse(性能评分与优化建议) :Google 推出的自动化性能分析工具,可生成综合性能报告
-
功能:
- 评估加载性能、交互响应性、SEO 等指标。
- 针对 React 应用,会检测代码分割(Code Splitting)、树摇(Tree Shaking)等优化点。
- 提供具体优化建议(如减少未使用的 JavaScript、优化图片加载)。
-
使用方式:
- Chrome DevTools → Lighthouse 面板。
- 命令行工具:
lighthouse https://your-app.com。
-
适用场景:整体性能评估,尤其是首屏加载性能优化。
-
-
why-did-you-render(检测不必要的重渲染) :一个专门用于 React 的库,能自动检测并警告不必要的组件重渲染
// 安装:npm install why-did-you-render --save-dev import React from 'react'; if (process.env.NODE_ENV === 'development') { const whyDidYouRender = require('why-did-you-render'); whyDidYouRender(React, { trackAllPureComponents: true, // 跟踪纯组件 }); }- 功能:当组件因 props/state 未变化而重渲染时,在控制台输出警告,说明重渲染原因(如父组件传递了新的引用类型 props)。
- 优势:比 React DevTools 更精准,能直接定位重渲染的触发原因,适合优化组件渲染逻辑。
-
23、react常用性能优化手段
-
React.memo:缓存组件渲染结果,只要props不变,组件就不会重新渲染
-
useMemo:缓存复杂计算的结果,只要依赖项不变,就不会重新计算
-
useCallback:缓存函数的引用,当函数作为子组件的props传递时,每次组件重新渲染都会重新生成函数,导致子组件接收的props变化,触发子组件不必要的重新渲染。
-
React.lasy:路由懒加载,只有在组件被访问到的时候,才会去加载解析对应模块的代码
-
SSR服务端渲染:渲染工作在服务端进行,客户端只需要展示服务端生成的静态页面,并处理水合(
hydration)过程就可以了,极大的减小了首屏加载时间 -
防抖、节流:减少不必要的渲染、或者函数执行,从而提升应用性能
-
虚拟列表react-window:只渲染用户看得见的地方
-
为列表配置合理的key:配合react的diff算法,提高页面的更新效率
-
react 18的并发特性API:
- useTranstion:标记优先级的渲染任务
- useDeferredValue:为值创建延迟版本
24、react虚拟列表怎么实现
虚拟列表(Virtual List)是处理大量数据列表渲染的高效方案,其核心原理是只渲染可视区域内的列表项,而非全部数据,从而大幅减少 DOM 节点数量和渲染开销。
实现方案:采用固定定位和绝对定位方式,在可视区域渲染真实数据,非可视区域,渲染一个空的div仅用来撑起高度,以展示出滚动条
第三方库方案:react-window
25、react中的代码分割
代码分割是 React 大型应用性能优化的关键手段,核心是利用 import() 动态导入语法,结合 lazy 和 Suspense 实现组件的按需加载。实际开发中,建议优先基于路由分割,再根据需求补充组件级分割,同时注意平衡代码块数量和加载体验。
26、SSR服务端渲染
在服务端计算和渲染生成静态页面,再发送给客户端,浏览器对js进行水合之后,绘制到屏幕上
优点:有利于SEO搜索引擎优化、减少首屏加载时间
27、什么是hydration
是服务端渲染(SSR)或静态生成(SSG)流程中的关键步骤,指的是将服务端返回的静态 HTML 转换为可交互的客户端 React 组件的过程。
工作原理:
-
服务端输出:服务端渲染时,React 会生成两部分内容:
- 静态 HTML 结构(用于快速展示)。
- 用于客户端 Hydration 的 “数据标记”(如
data-reactroot、data-reactid),帮助客户端识别 DOM 节点与虚拟 DOM 的对应关系。
-
客户端 Hydration 流程:
- 浏览器先渲染服务端返回的静态 HTML,用户看到初步内容。
- 客户端加载 React 运行时和应用代码。
- React 调用
hydrateRoot(React 18+)或hydrate(旧版)方法,传入服务端渲染的 DOM 根节点和组件树。 - React 对比服务端生成的虚拟 DOM 与客户端实际 DOM,建立映射关系,绑定事件监听器(如
onClick),恢复组件状态(如useState的初始值)。 - 完成后,页面从 “静态” 变为 “动态”,用户可以与组件交互。
28、react testing library
React Testing Library(RTL)是一个专注于以用户视角测试 React 组件的测试库,它的核心理念是:测试组件的行为而非实现细节。这种理念让测试更贴近真实用户体验,同时使测试更健壮(不易因组件内部实现变化而失效)。
29、react-hook-from
react-hook-form 是一个专注于性能和用户体验的 React 表单库,它通过利用 React Hooks 简化表单处理逻辑,同时提供高效的表单验证、状态管理和错误处理能力。与传统表单库(如 Formik)相比,它的核心优势是减少重渲染次数和更简洁的 API。
-
性能优化
- 采用非受控组件(Uncontrolled Components)的理念,减少不必要的重渲染(通过
ref而非state跟踪输入值)。 - 只在需要时更新表单状态,避免整体表单重渲染。
- 采用非受控组件(Uncontrolled Components)的理念,减少不必要的重渲染(通过
-
简洁的 API
- 基于 Hooks 设计,代码侵入性低,学习成本低。
- 最少的代码即可实现复杂表单逻辑。
-
强大的验证
- 内置支持 Yup、Zod 等验证库,也可自定义验证规则。
- 实时验证、提交验证、字段级验证等多种验证方式。
-
良好的类型支持
- 全面支持 TypeScript,提供类型安全的表单处理。
30、如何在react中优雅的使用css
- 中大型项目:优先选择 CSS Modules + Sass(平衡可控性和原生特性)或 Styled Components(动态样式需求多)。
- 快速开发 / 小型项目:Tailwind CSS 效率最高,适合追求速度和一致性的场景。
- 动态样式密集型场景:CSS-in-JS 库(如
styled-components)更灵活。
31、react的动画方案有哪些
- 原生css过渡、动画(transition
或@keyframes) - React Transition Group(官方推荐的过渡管理)
- React Spring 物理动画(第三方动画库)