React面试题

164 阅读21分钟

一、React声明式编程的优点

开发者只需要关注state就可以,相同的state总是会返回相应的UI描述,当出现bug的时候,你只需要去检查相应时间点的state是否正确,不用再去检查那些琐碎的dom操作

二、State改变后,react的工作流程

1 重新执行组件函数f 返回一个新的UI描述

2就是将新旧UI描述进行比较(协调的过程)

3 计算出最小的差异,并只更新真实dom中那些真正发生变化的部分

三、如何看待UI= f(state)?

1、精辟地概括了react的核心设计哲学,用户界面是应用状态的纯函数映射结果

2,State呢就是单一数据源驱动UI变化的唯一事实来源

F 就是函数组件,那根据state和props返回UI描述

UI就是虚拟dom对UI的一种轻量级描述

3、像jquery之前就是命令式ui,那他会手动逐步的去操作dom

那像react声明式UI,就是只声明特定状态下UI的样子,react负责更新,好处就是可预测性,关注点分离

4、react工作机制就是state的变化——重新执行组件——比较虚拟dom——高效更新真实dom

四、什么是虚拟dom

虚拟dom本质上是一个普通的JavaScript对象 是在应用程序逻辑和浏览器真实盗墓之间的一个重要抽象层和缓冲 它用来描述真实网页中dom树的结构和属性 可以看作是真实dom的一个蓝图或快照 它的特点就是轻量级 存在内存中不操作成本低

操作真实dom可能带来一些性能瓶颈,比如说大量连续的重排和重绘,会导致页面卡顿,用户体验下降

虚拟dom的好处就是跨平台开发,他是js对象,不直接依赖浏览器环境 所以可以应用在rn或者是ssr、支持声明式编程,开发者专注于数据和状态管理,提升开发效率

因为虚拟dom自身也有js的计算开销,首次渲染也会有轻微的延迟,也会有内存占用,所以说一些很简单的应用或页面能精确优化dom操作的时候,其实直接操作dom也可能更快

五、虚拟dom是如何工作的?

用js对象模拟虚拟dom状态变化时,先在js对象上计算和比较,找出最小的差异,然后才将这些差异批量更新到真实的dom 创建更新虚拟盗墓树 然后diff比较与调和reconciliation 批量更新真实dom 最终就是显著减少直接操作真实盗墓的次数和范围,提升复杂应用下的渲染性能

六、vite比webpack快在哪里

vite就是开发时用esbuild 生产构建用rollup 什么时候用web pack更有优势?

七、函数组件和类组件的区别

语法不同,状态管理机制不同,生命周期副作用不同 This的指向问题 获取props的方式 性能优化方式

类组件核心机制就是class语法,有this关键字,生命周期方法,用this.state this.setstate状态管理和组件实例 面向对象编程

函数组件就是个js函数,函数式编程,没有this 只有hooks 用useState去声明管理状态,代码简洁

使用hooks什么需要注意的地方吗?比如闭包陷阱?

在useeffect里面设置正确的依赖项

使用函数式更新 setstate

使用useref保存可变的值

八、react props 为什么不可变性?

1、React的核心设计就是单项数据流,如果子组件可以随意修改props,数据来源将会变得很混乱。

2、性能优化的关键.通过比较新旧props和state来决定组件是否需要重新渲染,那副组件数据更新时会传递一个全新的props对象(引用地址不同),只需要比较新旧purpose对象的内存地址是否相同就可以了,避免不必要的深比较。

3、提升组件的可预测性与调试效率

若界面显示异常,可更明确的去判断父组件传递props问题还是子组件自身的state管理有问题

4、利于做一些高级功能,比如说历史追踪等等

九、useState的函数式更新有什么好处?

1 保证状态更新的准确性(过时闭包)

setState(prevstate=>newstate) :prevstate 这个参数是当前最新的状态

setState(value) 这个state变量 可能是异步回调或多次调用的旧值

2 适用于状态更新,复杂逻辑或依赖先前状态的一个场景

如果新的状态需要基于前一个状态进行计算,比如计数器,快速连续事件,异步回调,函数是更新提供了一种更安全和声明式的方式来表达这种转换

3 函数是更新,因为其纯粹性它不依赖外部变量,只依赖传入的prevstate 有时能更好的配合react的内部优化机制

十、useEffact依赖项数组的理解

其核心目的是精确的控制副作用函数的执行时机,避免不必要的计算和API调用

1、不提供依赖项数组的话:会在每次组件渲染数据更新后都会执行

2、提供一个空数组,只会在组件首次挂载后执行一次,其返回的清理函数会在组件卸载时执行一次

3、提供一个包含依赖项的数组,首次挂载后执行,后续的每一次渲染后react会使用object.is()算法比较数组中的每一个依赖项的当前值和上一次的渲染时的值

Object is原始类型,比较的是值,引用类型比较的是引用地址

4、依赖项如果是函数的话,可以使用use call back包裹一下那个依赖项的函数

5、如果是复杂对象或数组的话,可以用usememo去包裹

useEffact可以是async吗?

不可以,因为async隐式返回promise,useEffact期望回调不返回或是返回清理函数,所以返回的promise会被当成清理函数。 可以在useEffact定义一个async函数,并立即调用它。

十一、useContext有哪些性能问题?

Provider的value更新,所以useContext的消费者都会重新渲染,即使依赖的数据没有改变。

因为 useContext 订阅的是整个context对象,React通过判断 对象的引用来判断变更。

优化方法:

可以拆分成单一的Context,使用useMemoa或是useCallback稳定value

十二、useState和useReducer区别

useState 和 useReducer 都是 React 里管状态的工具,区别主要看情况:

简单场景就用 useState,比如记个数字、开关状态,直接设值就行,简单好上手。

复杂情况就用 useReducer,比如状态多且有关联(像购物车增删改),或者更新逻辑复杂,它通过统一的 reducer 函数管理,逻辑更清楚,也方便维护。

简单说就是:简单状态用 useState,复杂状态用 useReducer。

优先用 useReducer
  • 状态是对象或数组,且包含多个子值(如 { user: {}, list: [], loading: false }
  • 状态更新依赖于前一个状态,或有复杂的条件判断
  • 组件内多个地方需要更新同一状态,且逻辑重复
  • 需要记录状态变更历史(如撤销 / 重做功能)

示例:管理一个购物车状态

十三、useMemo、useCallback的作用?

useMemo:

父组件重新渲染,所有子组件默认也会重新渲染。即使逻辑或内容未变,他们的引用地址也会发生改变。 React.memo进行浅比较时,会因引用地址改变而认为prop改变,导致子组件重新渲染。

useMemo就是react提供的性能优化hook,主要就是缓存计算结果

避免每次渲染时都进行不必要的,开销较大的重复计算。

接收一个函数和依赖项数组,依赖项发生变化时才重新计算,否则返回上一次缓存的结果。

useCallback

是react提供的性能优化hook,主要用于缓存函数实例 它返回回调函数的记忆化版本,该回调仅在某个依赖项发生改变时才会更新其引用

useCallback使用场景

1、将回调传递给优化的子组件,特别是用React.memo包裹的、依赖props引用相等性的子组件。 2、回调函数作为其他hook依赖项,如useEffect,useMemo,确保函数依赖项稳定,避免不必要重复执行。

经验法则

1、useCallback:子组件因对象、函数props引用变化而频繁不必要重渲染。 2、useCallback:useEffect 因函数依赖引用频繁变化而重复执行。 3、useMemo:计算逻辑 确实非常耗时(几十ms以上)且输入不常变化。

useCallback、useMemo的关系 useCallback(fn,deps) == useMemo(()=>fn,deps) useCallback看作是 useMemo的语法糖 useCallback 主要是为了语义上的清晰,表明正在记忆化一个函数

十四、useRef和useState

最根本的区别就是 是否会触发组件的重新渲染。

useState更新状态会触发渲染,界面反映最新状态。

useRef修改.current值 本身不导致组件重新渲染,ref对象生命周期内不变。

useRef设计目的

1、访问和操作dom,获取焦点,触 发动画,测量尺寸

2、存储可变的,与渲染无关的值,持久化数据但不触发渲染,如定时器id,上次props和state

十五、如何解释forwardRef

主要是解决父组件获取子组件内部特定dom元素或子类组件实例ref的场景。

默认情况下,函数组件不能直接通过props接收ref,ref属性会被react忽略。

forwardRef允许函数组件接收ref作为第二参数,并将期转发给内部子元素。

十六、如何解释useImperativeHandle?

与react.forwardRef配合,允许子组件自定义暴露给父组件的ref实例。

forwardRef传递ref,useImperativeHandle控制ref.current指向。

返回自定义对象,挂载特定方法/属性作为命令式api

好处:

增强封装性:隐藏内部实现,暴露最小必要接口。

api更清晰:调用意图明确。

更好的代码维护性:子组件内部重构不影响父组件。

十六、如何解释react18的并发特性

可中断,可调度

十七、react18中并发的useTranstion和useDeferredrValue

useTranstion: 标记不那么紧急的更新,控制状态更新的时机与优先级,避免阻塞优先级交互。(比如搜索/筛选大型列表) 允许将状态更新为过渡,降低其优先级。 返回isPending(布尔值)和startTransition(函数,用于包裹低优先级状态更新)

useDeferredrValue: 获取一个延迟的值(提供一个值的副本,此副本的更新被延迟,以避免阻塞主渲染,比如外部数据源的图表/可视化) 接收一个值 ,并返回该值的延迟版本 延迟版本的值 会在紧急更新(如用户输入)完成后才会更新。

十八、如何理解组件的 单一职责原则SRP

定义:一个组件,模块或类中,应该有且仅有一个引起它变化的原因(一个组件只做好一件事) 目的:提高内聚性,隆低耦合性,提供可维护性,增强可复用性,提升可测试性,代码也清晰。

十九、为什么组合大于继承?

组合是实现代码得用和构建灵活组件结构的首选。

react组件本质更像函数,组合更符合这种模型

组合提供了更好的灵活性,可维护性,降低了组件间的耦合度。

避免了传统继承可能带来的层级过深,方法覆盖混乱等问题

官方文档也指出 他们并未发现在react组件中使用继承解决不了的问题,而这些问题用组合能更好的解决。

如果有一些非常通用的,非ui相关的辅助方法,可能会考虑类继承吧。

二十、错误边界

捕获子组件js错误,展示降级ui,提升用户体验,增强应用健壮性。

核心方法,getDerivedStateFromError(更新state以渲染隆级ui) componentDidCatch(记录错误 )

二十一、如何设计一个优秀的组件库

开发者体验和最终用户体验并重。

不仅仅是单个组件,更是整体的解决方案。

考虑可扩展性,可维护性和社区生态。

二十二、什么是原子设计

一种从基础到复杂的ui构建方法论。

五个层级:原子-分子-组织-模板-页面

核心价值:一致性,复用性,可维护性,协作效率

二十三、如何实现一个受保护的路由

二十四、当应用需要全局状态,却又不想引入像 Redux 那样 heavy(繁重、庞大)的库时,可以用useContext + useReducer

核心做法:会考虑使用useContext结合useReducer。其中useReducer负责定义和管理状态变更的逻辑,useContext则把产生的state(状态)和dispatch(用于触发状态变更的函数)轻松共享给所有需要的子组件。

优点:代码简洁,是 React 原生(自带)的能力,易于理解和上手。

局限性 / 注意事项:

Contextvalue发生变化时,会导致所有使用该Context的消费者(子组件等)重新渲染,所以要注意优化。 对于超大型应用,可能缺少 Redux 的一些高级功能,比如中间件、强大的 DevTools(开发工具)。

总结

useContext:用于解决数据跨层级传递的问题。

useReducer:负责管理复杂的状态逻辑。

useContext + useReducer:是一种轻量、原生的 React 状态管理模式。

具体来说,通过 Provider 来封装 state(状态)和 dispatch(用于触发状态变更的函数);组件则通过 useContext(或者自定义 Hook)来消费这些状态和方法。

适用场景:这种状态管理模式适用于中小型应用,或者大型应用中的局部状态管理。

二十五、如何表达 RTK(Redux Toolkit)的优势

减少痛点:RTK 通过 createSlice 显著减少了传统 Redux 中繁琐的模板代码。

便捷性:configureStore 简化了 Store 配置,默认集成了 Redux Thunk 和 DevTools。

开发体验:内置 Immer.js,在 reducer 中可以直接修改对象,底层会自动保证不可变性。

标准化:createAsyncThunk 标准化了异步 action 处理,提高了代码一致性。

总结:RTK 让 Redux 更易用、简洁,是当前的最佳实践。

二十六、immer.js在redux的重要性

二十七、考虑使用 SWR 或 React Query 的场景

  • 频繁的异步数据交互:当应用大量依赖 API 来获取和展示数据时。
  • 需要缓存和后台同步:希望提升性能,确保数据的新鲜度。
  • 复杂的数据依赖和关系:例如,一个请求依赖另一个请求的结果。
  • 希望简化数据获取逻辑:减少样板代码,从而能更专注于业务逻辑。
  • 追求更好的用户体验:通过乐观更新、后台刷新等方式来实现。

二十八、rudux的中间件是如何工作的?

二十九、你通过哪些工具来定位react的的性能瓶颈呢?

  1. React DevTools Profiler:官方插件,可以分析组件渲染时间、渲染次数,快速定位不必要的重复渲染。

  2. 浏览器 Performance 面板:配合火焰图查看 JS 执行、Re-render、Repaint 的耗时,分析页面卡顿的根因。

  3. 日志 & 分析工具:在关键组件加 why-did-you-render 或 console 统计渲染次数,辅助定位问题。 总结:一般先用 Profiler 找“哪个组件渲染过多”,再用 Performance 看“为什么卡”,必要时用日志验证。

一句话版

👉 我会用 React DevTools Profiler 找重复渲染,结合 Chrome Performance 看整体性能,再配合 why-did-you-render 辅助分析。

三十、虚拟列表

虚拟列表的工作原理
  1. 容器(Container :具有固定高度,且样式设置为 overflow: auto/scroll(用于产生滚动效果)。

3.滚动内容区(Scrollable Content) :是一个内部 div,其高度等于所有列表项的总高度,作用是撑开滚动条。

4.可视区域计算:依据容器的 scrollTop(滚动距离)、容器高度,以及每个列表项的预估 / 固定高度,计算出当前可见列表项的 startIndex(起始索引)和 endIndex(结束索引)。

5.列表项渲染:仅渲染列表中 items.slice(startIndex, endIndex + 1) 范围内的项(即只渲染可视区域内的列表项)。

6.绝对定位:通过 position: absolute 和 top 样式,将渲染的列表项精准放置到滚动内容区内的正确位置。

7.滚动监听:监听容器的 scroll 事件,在滚动时重新计算可视区域,并更新列表项的渲染。

如何阐述虚拟列表
  1. 点出问题:当要渲染大量数据(比如几百、上千条甚至更多)的列表时,一次性渲染所有 DOM 节点会引发页面卡顿、内存占用过高、响应变慢等性能问题。
  2. 提出方案:为解决该问题,可采用虚拟列表技术。
  3. 核心原理:虚拟列表的核心思想是 “按需渲染” 或 “部分渲染”。它只渲染用户当前视口内可见的列表项,以及视口外少量用于缓冲的列表项。通过计算和维护一个 “视口窗口”,在用户滚动时,动态更新这个窗口内应该渲染的 DOM 元素。
  4. 实现关键:(此处内容未完全展示,推测会进一步讲解虚拟列表实现的关键技术点,比如之前提到的容器设置、滚动内容区、可视区域计算、列表项渲染、绝对定位、滚动监听等相关内容)
如何实现 / 使用虚拟列表:
  1. 自己实现?
  • 挑战:滚动事件处理、视口计算、item 复用、动态高度等逻辑复杂,容易出错。
  • 学习价值:有助于深入理解虚拟列表的原理。
  1. 成熟的第三方库(推荐):
  • react-window:轻量,推荐用于固定高度列表。
  • react-virtualized:功能更全面,支持动态高度、表格等,但体积稍大。
  • TanStack Virtual(原 react-virtual):无头 UI 库,高度可定制。

三十一、代码分割在react中的实现方式

  • 代码分割:按需加载代码,提升应用性能。
  • React 工具React.lazy() 用于定义动态加载的组件;Suspense 组件用于处理动态组件的加载状态(比如展示加载中提示等)。
  • 主要优势:让应用初始加载更快,带来更优的用户体验。
  • 常见场景:在路由级别进行代码分割(即不同路由对应的组件,在需要时才加载)。
  • 关键点:要合理选择代码分割的节点,同时妥善处理组件加载过程中的状态以及可能出现的错误状态。

三十二、SSR

  • 定义:在服务器端将客户端的应用程序(通常是用 JavaScript 框架编写的单页应用 SPA)渲染成完整的 HTML 字符串,然后将这个 HTML 直接发送给浏览器。
  • 核心思想:浏览器接收到的是可以直接显示的 HTML 内容,而不是等待 JavaScript 加载执行后再生成页面。
服务端渲染(SSR)为何重要
  • 解决两大痛点
    1. SEO 优化:搜索引擎爬虫可以直接抓取到完整的页面内容。
    2. 首屏加载速度(FCP/LCP) :用户能更快看到页面内容,提升体验。
  • 对比客户端渲染(CSR)
    • CSR(客户端渲染) :浏览器下载 HTML 骨架 -> 下载 JS -> JS 执行 -> 请求数据 -> 渲染页面。
    • SSR(服务端渲染) :浏览器请求 -> 服务器处理(数据 + 渲染) -> 返回完整 HTML -> 浏览器显示。
服务端渲染(SSR)的基本工作流程
  1. 用户请求:浏览器向服务器发起页面请求。

  2. 服务器处理

    • 数据获取:服务器(可能)需要先异步获取该页面所需的数据。
    • 渲染 HTML:在服务器端执行 JavaScript 代码,将组件渲染为 HTML 字符串。
  3. 响应 HTML:服务器将生成的完整 HTML 以及可能需要的初始数据(脱水数据)一并发送给浏览器。

  4. 浏览器渲染:浏览器接收到 HTML 后,立即解析并显示页面内容。

  5. 注水(Hydration) :JS 代码接管静态 HTML,添加事件监听器、恢复应用状态等,使页面变得可交互。

适用使用 SSR 的场景
  • 内容型网站:如博客、新闻门户、电商商品详情页,SEO(搜索引擎优化)至关重要。
  • 对首屏加载速度有极致要求的应用:提升用户留存和转化。
  • 目标用户网络环境较差或设备性能不一:确保基础体验。
可以不使用 SSR 的场景
  • 管理后台、内部系统等对 SEO 无要求,且用户网络环境通常较好的应用。
  • 纯静态站点(可考虑 SSG - 静态站点生成)。

三十三、非受控组件和受控组件的区别?

受控组件:表单数据由 React 的 state 管理,value 受控,输入变化通过 onChange 更新 state,适合需要校验、联动的场景。

非受控组件:表单数据由 DOM 自己维护,用 defaultValue 初始化,通常通过 ref 获取值,写法简单,适合一次性收集数据。

一句话记忆:

👉 受控 = 数据在 React非受控 = 数据在 DOM

三十四、react-hook-form比传统表单的优势

  1. 性能更好:基于 非受控 组件,避免频繁 re-render。
  2. 代码更简洁:不用手动写 onChange 和 setState,字段注册即可收集数据。
  3. 验证更强大:内置校验规则,还能无缝结合 Yup/Zod 做复杂校验。 👉 总结:传统表单繁琐且性能差,React Hook Form 更轻量、高效,尤其适合大表单。

三十五、react中你常用的css方案是什么?各自的优缺点是什么?

  • 回答立场:没有万能的技术方案,CSS 方案的选择要依据业务场景和团队背景。

  • 方案分类阐述

    • CSS Modules:构建时生成唯一类名,能实现样式隔离,接近原生 CSS,使用成本低,但动态样式能力较弱。
    • CSS-in-JS:通过 JavaScript 驱动样式,支持动态样式,组件与样式内聚性强,不过运行时可能存在性能问题。
    • Tailwind CSS:基于原子类的样式方案,开发效率高,能对样式进行统一约束,但会使 JSX 代码显得臃肿,且有一定学习成本。
  • 场景化建议:追求稳定、渐进式改造选 CSS Modules;

  • 追求极致组件化和动态性选 CSS-in-JS;

  • 追求开发效率和统一设计选 Tailwind CSS。

  • 加分展望:可提及 CSS 官方的 @scope 提案,以及 Panda CSS、StyleX 等 “零运行时” 的 CSS-in-JS 方案。

  • 核心总结:CSS Modules 是最接近原生 CSS 的组件化方案,胜在稳定和无感;

  • CSS-in-JS 是将组件化思想贯彻到底的方案,强在动态和内聚;

  • Tailwind CSS 是改变开发范式的方案,赢在效率和约束。

三十六、动画方案

    • React Transition Group:是管理 CSS 类名的状态机,比较轻量,适合简单的过渡效果。
    • Framer Motion:采用声明式 API,生产力高,是通用 UI 动效和手势交互的首选。
    • React Spring:基于物理模型,动画效果自然,适合复杂且高交互性的动画。
  • 选型思路:按需选择,从简单的方案开始逐步到复杂的。
  • 具体策略是,先尝试用 React Transition Group,当它不能满足需求时,转向 Framer Motion,若 Framer Motion 的预设模型也无法满足特定的物理交互需求,再使用 React Spring