React面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 22:54

React Native 调试有哪些方法?各调试工具分别适用什么场景?

React Native 调试分 JS 层调试、原生层调试、性能分析 三个层面。JS 层调试: Chrome DevTools 是最基础的方式,通过 Ctrl+M 打开开发者菜单选择 Debug 打开 Chrome 调试。但 Chrome 调试运行在 Chrome V8 而非设备 JSC/Hermes 上,存在兼容差异。推荐使用 Flipper,它内置 React DevTools 和网络检查器,支持断点、Console、组件树检查,是官方推荐的调试平台。原生层调试: Android 用 Android Studio 的 Logcat 过滤 ReactNativeJNI 日志,可对原生模块设断点调试;iOS 用 Xcode 的 LLDB,在 RCTBridge 方法处打断点。性能分析: Perf Monitor(开发者菜单内)查看 FPS 和内存;Systrace 记录帧渲染时间线定位掉帧原因;Hermes 引擎自带 CPU Profiler 生成 Chrome Trace 格式文件。React DevTools Profiler 分析组件渲染耗时和频率。常用工具总结: Flipper(全能调试)、React DevTools(组件树)、Chrome DevTools(JS 断点)、Xcode/Android Studio(原生调试)、Systrace(性能分析)。 追问: Chrome 调试模式和 Hermes 引擎调试有什么区别? Flipper 在新架构下是否还适用?有什么替代方案? 如何对 React Native 的启动耗时进行量化分析? 生产环境如何收集 JS 异常和崩溃日志? Hermes 的 CPU Profiler 生成的 trace 文件如何分析?
服务端阅读 05月29日 22:54

React Native 性能优化怎么做?常见性能瓶颈如何排查和解决?

React Native 性能优化的核心是 减少 JS Bridge 通信、控制重渲染、优化列表滚动 三个方向。减少 Bridge 通信: Bridge 是异步串行的,高频调用会积压队列。将批量操作合并为一次原生调用,使用 Native Modules 批量传递数据而非逐条传递。新架构 Fabric 取代了异步 Bridge,实现同步通信。控制重渲染: 使用 React.memo 包裹子组件避免不必要的 re-render;用 useMemo 缓存计算结果、useCallback 缓存回调函数;列表项拆分为独立组件并 memo 化。避免在 render 中创建内联对象和函数。优化列表: FlatList 替代 ScrollView 渲染长列表,设置 keyExtractor、getItemLayout 提升性能;windowSize 控制渲染窗口大小;removeClippedSubviews 回收不可见视图。VirtualizedList 是 FlatList 的底层,可做更细粒度控制。其他关键点: 图片使用缓存库(如 react-native-fast-image);动画用 useNativeDriver 驱动跑在原生线程;避免在主线程做 JSON 解析等耗时操作;Profiling 工具用 Flipper 或 Systrace 定位瓶颈。 追问: React.memo 的浅比较在什么场景下会失效?如何自定义比较逻辑? FlatList 的 windowSize 参数设为多少合适?过小会有什么副作用? useNativeDriver 为什么不能驱动布局属性(如 height)的动画? 新架构 Fabric 和 TurboModules 相比旧 Bridge 的性能提升体现在哪里? 如何用 Systrace 定位 JS 线程和原生线程的耗时瓶颈?
服务端阅读 05月29日 01:38

Zustand 中间件怎么使用?有哪些内置中间件?

Zustand 中间件以函数组合方式包裹 create 的回调,从内到外依次嵌套。内置三个核心中间件:persist(状态持久化到 localStorage/sessionStorage)、devtools(接入 Redux DevTools 调试)、immer(简化不可变更新,可直接写 state.user.name = 'new')。组合顺序:devtools 在最外层,persist 在内层,中间件顺序影响 set/get 的拦截链。追问persist 的 partialize 怎么过滤不需要持久化的字段?partialize: (state) => ({ user: state.user }) 只持久化 user,token 等敏感或临时字段不会写入存储。反序列化时缺失字段会使用 create 中的初始值填充。immer 中间件解决了什么问题?React 要求状态不可变更新,深层嵌套需逐层展开 {…s, user: {…s.user, name: 'new'}},代码冗长易出错。immer 让你直接赋值 state.user.name = 'new',内部通过 Proxy 生成新对象。中间件组合顺序有影响吗?有。devtools(persist(fn)) 中 persist 在内层,持久化后的状态变化才会被 devtools 捕获;反过来的话 devtools 记录的是持久化前。一般推荐 devtools 在外、persist 在内。如何自定义中间件?中间件本质是高阶函数:(config) => (set, get, api) => config(fnSet, fnGet, api),在 fnSet/fnGet 中插入自定义逻辑(如日志、节流、权限校验),然后调用原 set/get。persist 持久化的状态怎么版本迁移?persist 配置中提供 migrate 函数:migrate: (persisted, version) => { if (version === 0) return { …persisted, newField: 'default' }; return persisted; },配合 version 字段标识当前版本,自动执行迁移。写段代码import { create } from 'zustand'import { persist, devtools } from 'zustand/middleware'const useStore = create( devtools( persist( (set) => ({ token: '', setToken: (t: string) => set({ token: t }), }), { name: 'auth', partialize: (s) => ({ token: s.token }) } ), { name: 'AuthStore' } ))
服务端阅读 05月29日 01:38

Zustand 中如何用 TypeScript 定义 Store 类型?

定义一个包含状态和方法的 interface,作为 create 的泛型参数传入即可:create((set, get) => ({…}))。TypeScript 会自动推断 set 回调中 state 的类型,方法参数也能正确约束。关键点:状态和方法写在同一个 interface 中,方法参数可引用自身类型(如 setUser: (user: StoreState['user']) => void)。追问中间件会改变 store 类型,怎么处理类型组合?persist/devtools 等中间件会向 store 注入额外属性(如 persist.clearStorage),需定义扩展类型 type Store = StoreState & StorePersist,再用 create() 包裹。zustand v4+ 的中间件泛型签名已支持自动推断,大部分场景无需手动拼接。StateCreator 泛型怎么用?StateCreator 用于拆分 store 逻辑时约束每个切片的类型,Mutators 描述中间件链对 set/get 的改写,U 是未包装的原始类型。手动组合切片时需要用到。selector 的返回类型怎么保证?useStore(s => s.count) 自动推断为 number。自定义 selector 返回派生值时,TypeScript 能推断返回类型;如果需要显式标注,写 useStore((s) => s.items.length)。泛型 store 怎么定义?function createSlice() 返回 StateCreator>,通过泛型参数让切片适配不同实体(如 createSlice()、createSlice()),避免为每个实体写重复逻辑。set 的 partial 参数类型为什么不是 Partial?set 接受 Partial 或 (state) => Partial,但嵌套对象是浅合并,深层更新需展开:set(s => ({ user: { …s.user, name: 'new' } })),否则丢失同级字段。写段代码interface BearState { bears: number inc: (n: number) => void}const useBearStore = create<BearState>((set) => ({ bears: 0, inc: (n) => set((s) => ({ bears: s.bears + n })),}))// 组件中const bears = useBearStore((s) => s.bears) // number
服务端阅读 05月29日 01:38

Zustand 是什么?相比 Redux 有什么优势?

Zustand 是一个极简 React 状态管理库,核心 API 只有 create():传入一个返回状态对象的函数,返回一个 hook,组件通过 const count = useStore(s => s.count) 按需订阅,未变化的部分不会触发重渲染。不需要 Provider 包裹,不需要 reducer/action 分发,setState 直接更新。体积约 1KB,零依赖。追问Zustand 的 selector 如何避免不必要的重渲染?useStore(selector) 只订阅 selector 返回的切片,内部用 Object.is 浅比较判断是否变化。引用类型可传第二个参数 shallow 比较函数,或用 useShallow。和 Jotai/Recoil 的原子化方案有什么区别?Zustand 是单一 store(可拆分),状态集中管理;Jotai/Recoil 是原子化,每个状态独立。Zustand 更适合关联性强的状态,原子化适合独立派生状态。没有 Provider 怎么实现组件间共享状态?Zustand store 本质是一个模块级闭包,所有引用同一 useStore 的组件共享同一个状态引用,不依赖 React Context 传递。create() 返回的 hook 能在组件外使用吗?可以。useStore.getState() 获取快照、useStore.setState() 更新状态,均可在非组件代码(如 axios 拦截器、WebSocket 回调)中使用,这是相比 useContext 的显著优势。多 store 还是单 store?Zustand 不限制,实践中按领域拆分多个 store 更常见,避免单个 store 臃肿,也便于按需加载和测试。写段代码import { create } from 'zustand'const useStore = create((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })),}))function Counter() { const count = useStore((s) => s.count) return <button onClick={useStore.getState().inc}>{count}</button>}
服务端阅读 05月29日 01:22

React Query 性能优化的常见瓶颈和解决方案有哪些?

React Query 最大性能陷阱是组件过度渲染:query 数据变化时所有订阅组件都重渲染。解法是用 select 提取组件关心的子字段,select 返回值用浅比较去重,相等则跳过渲染。第二陷阱是缓存策略不当:staleTime 控制数据何时标记为过期触发重新请求(默认0即立即过期),gcTime(v5 前叫 cacheTime)控制未使用的缓存何时被垃圾回收(默认5分钟)。不常变的数据应设较长 staleTime 避免无谓请求。第三是 queryKey 设计:key 变化就触发新请求,key 中包含频繁变化的值(如时间戳)会导致缓存失效。queryKey 应稳定且分层:['users', 'list', { status: 'active' }]。追问staleTime 和 gcTime 有什么区别?staleTime 决定数据是否需要重新获取(过期前用缓存,过期后下次挂载或窗口聚焦时 refetch);gcTime 决定缓存数据在内存中保留多久,观察者归零后开始倒计时,到期彻底删除。一个管新鲜度,一个管生命周期。select 怎么避免不必要的渲染?select 每次查询都会执行,但只有返回值与上次浅比较不同时才触发渲染。返回新对象/数组每次都是新引用会失效,需确保返回原始值或用结构化分享的子集。useInfiniteQuery 如何优化?每页数据独立缓存,默认所有页面变化都触发渲染。可用 select 只取当前视口需要的数据;配合虚拟滚动(如 react-virtual)避免渲染长列表;getNextPageParam 返回 undefined 自动停止加载。如何实现乐观更新?用 useMutation 的 onMutate 在请求发出前乐观修改缓存(queryClient.setQueryData),onError 时用 onMutate 保存的快照回滚,onSettled 时 invalidate 相关 query 保证最终一致。写段代码// select + staleTime 减少渲染和请求const { data: name } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id), staleTime: 5 * 60 * 1000, select: (data) => data.name,});
服务端阅读 05月29日 01:20

React Query 的缓存机制是如何工作的,如何配置和管理缓存?

React Query 缓存的核心是两个时间参数:staleTime 决定数据何时被标记为"过期"(默认 0,即立即过期),gcTime(原 cacheTime)决定数据何时被垃圾回收(默认 5 分钟)。数据在 staleTime 内不会重新请求,但窗口聚焦时会触发 refetch;超过 gcTime 且无观察者的查询会被从缓存清除。管理缓存的三个关键 API:queryClient.invalidateQueries() 标记失效并触发重新获取、queryClient.setQueryData() 直接更新缓存数据、queryClient.removeQueries() 彻底移除缓存条目。对于需要持久化的场景,可借助 persistQueryClient 将缓存写入 localStorage。追问staleTime 设为 Infinity 意味着什么?什么场景下合理?invalidateQueries 和 resetQueries 的区别是什么?如何利用 queryKey 层级结构实现局部失效(如只刷新某个用户下的 todo 列表)?窗口聚焦 refetch 在移动端或 Electron 中表现如何?如何关闭?持久化缓存到 localStorage 时如何处理敏感数据和版本不一致问题?写段代码const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60_000, gcTime: 30 * 60_000 } }})// mutation 成功后局部失效const mu = useMutation(updateTodo, { onSuccess: () => queryClient.invalidateQueries(['todos'])})
前端阅读 495月28日 03:32

React 组件抽离公共逻辑代码有哪些方式?

React 逻辑复用经历了三代方案的演进:Mixin → HOC / Render Props → Hooks。Mixin 已随 Class 组件淘汰,当前面试重点在后面三种。HOC(高阶组件)函数接受一个组件,返回增强后的新组件:function withAuth(WrappedComponent) { return function AuthComponent(props) { const isAuthenticated = checkAuth(); return isAuthenticated ? <WrappedComponent {...props} /> : <Navigate to="/login" />; };}// 使用const ProtectedPage = withAuth(Dashboard);核心问题:Wrapper Hell:多层 HOC 嵌套后,DevTools 里组件树极深,调试困难Props 来源不透明:<WrappedComponent {...props} /> 透传的 props 来自哪里不直观,容易命名冲突静态方法丢失:HOC 返回新组件,原组件的静态方法不会自动复制,需要 hoist-non-react-statics 手动提升Ref 丢失:ref 不属于 props,会被绑定到外层 HOC 组件而非原组件,需配合 React.forwardRef 转发Render Props组件接受一个返回 React 元素的函数 prop,由该函数决定渲染内容:function Mouse({ render }) { const [pos, setPos] = useState({ x: 0, y: 0 }); useEffect(() => { const handler = (e) => setPos({ x: e.clientX, y: e.clientY }); window.addEventListener(mousemove, handler); return () => window.removeEventListener(mousemove, handler); }, []); return render(pos);}// 使用<Mouse render={pos => <Cursor pos={pos} />} />核心问题:嵌套地狱:多个 Render Props 嵌套时,回调层级极深,可读性急剧下降性能隐患:每次父组件渲染,render 函数都会重新创建,导致子组件不必要的重渲染,需要额外做 useCallback 优化Hooks(推荐)在函数组件内调用自定义 Hook,逻辑与 UI 完全分离,无组件层级嵌套:function useAuth() { const [user, setUser] = useState(null); useEffect(() => { const unsub = onAuthStateChanged(setUser); return unsub; }, []); return user;}// 使用function Dashboard() { const user = useAuth(); if (!user) return <Navigate to="/login" />; return <main>...</main>;}Hooks 的注意事项:不能在条件语句、循环或嵌套函数中调用——React 依靠调用顺序匹配 Fiber 链表上的 Hook 节点闭包陷阱:useEffect 内部如果引用了 state 但未加入依赖数组,回调中捕获的始终是旧值,需用 useRef 或函数式更新 setState(prev => prev + 1) 解决三种方案对比| 维度 | HOC | Render Props | Hooks ||------|-----|-------------|-------|| 组件嵌套 | 多层包裹 | 回调嵌套 | 无嵌套 || Props 透明度 | 来源不透明 | 显式传递 | 显式调用 || 类型推导 | 困难(泛型丢失) | 较好 | 好 || 适用场景 | 旧代码维护、Class 组件 | 旧代码维护 | 新代码首选 |三种方式的核心思想一致——把可复用逻辑从 UI 中分离。Hooks 胜在零组件嵌套、逻辑内聚、类型友好,是当前最佳实践。追问为什么 Hooks 不能放在条件语句里?React 用 Fiber 节点上的链表结构存储 Hook 状态。每次渲染时,Hook 按调用顺序依次匹配链表上的节点。如果某个 Hook 在某次渲染被跳过,后续 Hook 就会错位匹配到前一个 Hook 的状态节点,导致状态混乱。这是 React 内部实现机制决定的,而非 API 设计限制。HOC 还用在哪些场景?React.memo(性能优化,浅比较 props)connect(mapStateToProps, mapDispatchToProps)(Redux v5 以前)withRouter(React Router v5)权限控制:withAuth(ProtectedComponent)日志/埋点:withTracker(InteractiveComponent)如何把 Class 组件中的 HOC 迁移到 Hooks?| HOC 模式 | Hooks 替代 ||----------|-----------|| withRouter | useNavigate() + useLocation() + useParams() || connect() | useSelector() + useDispatch() || withAuth | 自定义 useAuth() || withTracker | 自定义 useTracker() + useEffect || 通用 HOC | 自定义 Hook + 组件内直接调用 |Hooks 有哪些常见陷阱?闭包陷阱:useEffect 中引用了 state 但依赖数组遗漏,回调拿到旧值。用 useRef 存最新值或函数式更新解决无限渲染:useEffect 依赖项传入每次新建的对象/数组引用,用 useMemo 稳定引用依赖缺失:遗漏依赖导致 effect 不按预期执行,启用 eslint-plugin-react-hooks 的 exhaustive-deps 规则自动检查
前端阅读 335月28日 03:31

React setState 是同步还是异步?原理是什么?

setState 并非真正“异步”——它是批量延迟执行。调用 setState 时,React 把更新对象推入当前 Fiber 节点的 updateQueue(环形链表),然后调度一次重新渲染,而不是立即修改 state 和触发 DOM 更新。等调度机制在下一个工作单元执行时,才遍历 updateQueue 计算新 state 并渲染。批量更新的核心逻辑:同一个事件循环内的多次 setState,只会产生一次渲染调度。合并发生在遍历 updateQueue 阶段——React 依次执行每个 update 的计算函数,得到最终 state,再进入 render。// 直接值更新:三次调用,updateQueue 里三个 update// 但都基于同一个闭包中的 count,最终 count = 0 + 1 = 1setCount(count + 1);setCount(count + 1);setCount(count + 1);// 函数式更新:每个 update 拿到前一个 update 的结果// count = ((0+1)+1)+1 = 3setCount(c => c + 1);setCount(c => c + 1);setCount(c => c + 1);追问setState 是同步还是异步?都不是。它本质是同步入队 + 延迟渲染。React 17 中,事件处理器外(setTimeout、原生事件)没有批量机制,setState 后能立即在同步代码中读到新值,看起来像“同步”——但这不是真正的同步,只是批量边界不同。React 18 用 createRoot 统一了所有场景的批处理。React 18 自动批处理的原理是什么?React 18 引入新调度入口 scheduleUpdateOnFiber,替代了原来的 enqueueSetState。无论更新来自事件处理器、setTimeout 还是 Promise,都走同一条调度路径。内部用 lane 模型(替代 expiration time)管理优先级,Scheduler 模块按优先级安排回调执行时机,实现所有场景统一批处理。什么情况下 setState 后组件不会重新渲染?bailout 机制:React 在渲染前会比较新旧 state(浅比较)。如果值没变,直接跳过该组件的渲染。常见陷阱——setState(obj) 传入同一个引用,浅比较相等,不会触发更新。必须创建新对象:setState({...obj, key: newVal})。函数式更新什么场景必须用?依赖前一个 state 时必须用。典型场景:计数器、队列操作(往数组追加元素)。函数式更新能保证每次拿到的都是链表中上一个 update 计算后的最新值,而不是闭包捕获的旧值。
前端阅读 455月28日 03:20

React 项目中常见的内存泄漏场景有哪些?

核心场景React 组件卸载后,与之关联的副作用如果没有同步清除,就会产生内存泄漏。实际项目中主要分四类:异步操作更新已卸载组件的状态在组件内发起 fetch 请求,组件卸载了请求才回来,此时 setState 会触发 React 的 warning(React 18 下已移除该 warning,但逻辑上的泄漏仍然存在)。setTimeout/setInterval 同理——组件卸载了定时器还在跑,回调里访问了过期的 state 或调用 setState。// 泄漏写法useEffect(() => { fetch('/api/data').then(res => res.json()).then(setData);}, []);// 修复:用 AbortController 取消请求useEffect(() => { const controller = new AbortController(); fetch('/api/data', { signal: controller.signal }) .then(res => res.json()) .then(setData) .catch(err => { if (err.name !== 'AbortError') throw err; }); return () => controller.abort();}, []);事件监听未移除在 useEffect 里给 window、document 或 DOM 节点添加了 resize、scroll、keydown 等监听,但 cleanup 里没有 removeEventListener。监听回调持有组件作用域的引用,组件实例无法被 GC 回收。useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize);}, []);闭包持有大对象引用useCallback/useMemo/useRef 的闭包里引用了不再需要的大对象(比如接口返回的完整列表、Blob 数据等),只要闭包还在,这些对象就无法被回收。常见于列表页缓存整个数据集却只展示部分。// 泄漏:闭包引用了全量数据const filteredList = useMemo(() => largeList.filter(fn), [largeList]);// 优化:只保留需要的字段,尽早释放原引用const filteredList = useMemo( () => largeList.map(item => ({ id: item.id, name: item.name })).filter(fn), [largeList]);未清理的订阅与第三方实例WebSocket、EventSource、IntersectionObserver、ResizeObserver、MutationObserver 这些 API 都需要手动 close()/disconnect()。第三方库(如 Redux 的 subscribe、RxJS 的 Observable.subscribe、ECharts 实例)同理,组件卸载时必须调用对应的销毁方法。useEffect(() => { const ws = new WebSocket('wss://example.com'); ws.onmessage = (e) => setMessage(JSON.parse(e.data)); return () => ws.close();}, []);修复思路核心原则:useEffect 的 setup 和 cleanup 必须对称——setup 里获取了什么资源,cleanup 里就要释放什么。具体做法:异步请求用 AbortController 取消定时器用 clearTimeout/clearInterval 清除DOM 事件用 removeEventListener 移除订阅类 API 调用 close()/disconnect()/unsubscribe()大对象引用用 useRef 配合手动置 null 释放追问useEffect 的 cleanup 什么时候执行?组件卸载时,或者下一次 effect 执行前(依赖项变化时先跑旧 cleanup 再跑新 effect)。React 18 StrictMode 下开发模式会额外执行一次 setup+cleanup 来暴露遗漏的清理逻辑。怎么排查 React 内存泄漏?Chrome DevTools → Memory 面板 → 取 Heap Snapshot。操作组件(挂载→卸载→挂载→卸载),对比两次 snapshot。如果 Detached DOM 节点或组件实例数量持续上升,说明有泄漏。也可以用 React DevTools Profiler 观察 React 组件树是否出现已卸载组件仍存在的现象。React 18 对内存泄漏有什么影响?React 18 移除了 "Can't perform a React state update on an unmounted component" 的 warning,但泄漏本身并未消失——未清理的闭包引用和事件监听仍然存在。同时 StrictMode 在开发模式下双重挂载组件,更容易暴露 cleanup 遗漏的问题。此外并发特性(Suspense、useTransition)引入了组件挂载-卸载-重新挂载的新生命周期,对 cleanup 的正确性要求更高。