前端面试只要往深里问React绝对是绕不开的一座大山。这两年我面试别人、也被别人面发现八股文背得再熟真正问到源码原理、框架设计取舍、边界情况处理的时候很多人还是会露馅。整理这份React面试向笔记不只是给面试前临时抱佛脚用更是想帮你把零散的知识点串成体系——JSX为什么这样设计、Hooks背后的闭包机制、组件通信的完整解法、性能优化到底该怎么做这些东西搞透了不管面的是初级还是资深岗心里都有底。这份内容适合准备前端面试的同学、带新人的技术组长以及想系统梳理React知识框架的开发者。全文按面试高频模块拆解每个小节都配了“面试官为什么这么问”的视角以及我实际面试中总结的追问方向建议你配合本地代码实验一起看。1. 核心概念串讲虚拟DOM、Fiber与JSX的本质1.1 虚拟DOM与diff算法为什么要多这一层先说结论虚拟DOM不是为了“比直接操作DOM更快”而是为了在“声明式UI”和“高效更新”之间找到一个平衡点。这个点我在面试中问过很多人十有八九会回答“虚拟DOM快”其实这个说法不够准确。浏览器直接操作DOM的代价不仅在于修改样式和内容本身更在于触发layout、paint甚至composite这些过程的开销是累积的。React引入虚拟DOM后开发者只需要描述“UI应该长什么样”React负责对比新旧虚拟DOM的差异计算出最小变更集再批量操作真实DOM。计算差异这个阶段是在JS层做的比直接触达真实DOM快一个数量级。但更重要的设计意图是把“DOM操作”抽象成一个可复用的层。因为有了虚拟DOMReact才能跑在浏览器、服务端SSR、甚至React Native的宿主环境上。真实DOM不是React的直接依赖而是“目标宿主”之一。这个抽象层的存在才是虚拟DOM最大的价值。diff算法的核心逻辑一句话总结是“同层对比、类型优先、key辅助复用”。具体规则我在实战中验证过无数次同层比较不跨层级复用节点标签类型不同直接重建比如div换成spankey相同且类型相同的节点走复用更新逻辑key不同或类型不同删除旧节点、创建新节点面试问到这里通常会追加一句“那key为什么要用唯一id而不是index”这就引到了列表渲染的坑。用index做key在列表头部插入元素时React会认为所有item的key没变只是内容变了于是逐一复用组件实例结果所有输入框的状态、图片的懒加载状态可能全部错乱。我自己踩过一次——一个动态表单在中间插入一行后面所有输入框的已填内容全乱了。从那次之后我统一用后端返回的业务ID或者自生成的nanoid作key。1.2 JSX不是HTML它是个语法糖JSX能写进React组件里本质是因为babel或tsc、swc把JSX语法编译成了React.createElement调用。这个转换过程是面试中最容易被忽略的考点因为平时写代码根本看不到这一层。举个例子const element div classNamebox你好/div实际编译成const element React.createElement(div, { className: box }, 你好)如果你用React 17babel会使用新的jsx runtime会自动从react/jsx-runtime导入jsx函数这时甚至可以不需要显式import React。但如果你还在字面上“React.createElement”就要清楚知道这是老runtime的行为。知道这个本质之后很多“为什么”就顺理成章了为什么JSX里不能用if语句因为JSX只是表达式不是语句if没有返回值没法嵌入到元素树里。所以你需要三元表达式、或立即执行函数。为什么class要写成className因为JSX编译后是JS对象“class”在JS里是保留字。为什么属性名要遵循camelCasetab-index这类带连字符的属性名在JS对象属性访问里要写成tabIndex。我在面试中常让候选人现场转换一段简单JSX并追问“这个createElement返回的对象大概长什么样”。只有真正理解JSX编译结果的人才能脱口而出——返回的是一个描述节点的plain object包含type、props、key、ref等字段。这也是后面说“虚拟DOM就是一棵由JS对象组成的树”的直接基础。1.3 组件化思维函数组件与类组件的本质差异很多候选人说起“函数组件和类组件区别”能背出一堆但问到“为什么函数组件更好”就容易卡住。我的理解是函数组件的核心优势不是“少写几行代码”而是让组件逻辑更纯粹、更容易预测。类组件里的this指向是动态绑定的事件处理函数里忘记bind就报错state更新是批量还是同步的在不同React版本里表现还不一样。函数组件直接把“props in → JSX out”这个映射关系显式化配合Hooks把状态和副作用从组件生命周期里抽离出来逻辑复用变得更自然。但这不代表类组件一无是处。老项目中大量类组件仍然在正常工作。我面试时不会问“你更喜欢哪个”而是问“如果让你维护一个类组件项目hooks能在里面用吗”——答案是不能直接混用的hooks只能在函数组件或自定义Hook里使用。这个问题看起来基础其实是在考察对React运行机制的理解边界。还有个高频追问组件是“函数”还是“类”对性能有影响吗严格说没有本质差别但函数组件配合React.memo做优化比类组件配合PureComponent更直观写法上也更简洁。后面性能优化章节再展开。2. Hooks深度解析高频考点与易错点2.1 useState与useEffect闭包陷阱与依赖数组Hooks之所以难是因为它完全建立在闭包机制之上。useState每次渲染都会生成一个新的state快照useEffect回调里捕获的也是某一次渲染时的props和state这就是“闭包陷阱”的根源。最经典的例子function Counter() { const [count, setCount] useState(0) useEffect(() { const timer setInterval(() { setCount(count 1) }, 1000) return () clearInterval(timer) }, []) }这段代码的问题是count在effect创建时被捕获为0之后每次setCount(0 1)count永远停在1。正确写法是使用函数式更新setCount(c c 1)这样不依赖外部状态闭包陷阱自然消失。很多初学者死记“依赖数组要写全”但没理解为什么。这里关键在于effect的回调函数只“看见”它创建那一刻的props和state。如果依赖数组漏了某个变量回调里会拿到过期值如果依赖数组在每次渲染时都变化effect会每次渲染都执行可能引发死循环。我实际面试时的杀手锏追问是“如果我在useEffect里发起请求但组件卸载了响应回来之后setState会不会报错”——React 18之后这个操作不再提示warning也不会导致内存泄漏但依然是不规范的行为。正确做法是使用一个ignore标志位或在effect的清理函数里取消请求。useEffect(() { let ignore false fetchData().then(data { if (!ignore) setData(data) }) return () { ignore true } }, [])2.2 useMemo与useCallback别为了优化而优化面试中对性能优化考察的重点之一就是useMemo、useCallback的判断力。新人刚学会这两个Hook喜欢到处包一层“防止重新渲染”结果反而适得其反。先说底层设计useMemo缓存计算结果useCallback缓存函数引用。它们的本质都是用空间换时间、用“缓存”换“稳定引用”。但缓存是有成本的——React需要在内存里保存依赖数组和结果还要在每次渲染时做依赖比对。如果一个计算本身很快或者一个函数没有作为依赖传给子组件包一层纯属浪费。我的判断标准非常简单计算量大且依赖项不频繁变化 —— 用useMemo会把函数传给子组件且子组件用了React.memo —— 用useCallback其他情况先不优化面试中还有个进阶问法“useMemo和useCallback能不能替代React.memo”答案是“不能完全替代”。useMemo缓存的是值useCallback缓存的是函数本身而React.memo决定的是“当前组件是否需要重新渲染”。它们解决的问题不同。再补充一个容易答错的点useMemo里执行副作用代码比如请求数据会怎样从规则上讲React允许这么写但语义上不应该——如果缓存的值没变副作用就不会执行你等于把一个可能有时效性的请求“冻结”了。求值逻辑应该保持纯净副作用必须放useEffect里。2.3 自定义Hook逻辑复用面试题的标准解法自定义Hook不仅能复用state逻辑还能复用一堆副作用和衍生逻辑。面试题里常见的“封装一个获取窗口尺寸的Hook”、“封装一个请求数据的Hook”、“封装一个防抖Hook”都是这一类。防抖Hook是个很好的练手例子function useDebounce(value, delay 300) { const [debouncedValue, setDebouncedValue] useState(value) useEffect(() { const timer setTimeout(() setDebouncedValue(value), delay) return () clearTimeout(timer) }, [value, delay]) return debouncedValue }这个Hook好在它同时展示了几件事自定义Hook内部可以用所有内置Hooks返回值可以是一个值、一个对象或一个数组清理函数可以避免上一个计时器干扰下一个。再展示一个“请求数据Hook”的完整版这也是我让候选人手写频率最高的代码题function useFetch(url) { const [data, setData] useState(null) const [loading, setLoading] useState(true) const [error, setError] useState(null) useEffect(() { let ignore false setLoading(true) fetch(url) .then(res { if (!res.ok) throw new Error(请求失败) return res.json() }) .then(data { if (!ignore) { setData(data) setError(null) } }) .catch(err { if (!ignore) setError(err.message) }) .finally(() { if (!ignore) setLoading(false) }) return () { ignore true } }, [url]) return { data, loading, error } }自定义Hook对面试的加分点在于你能否说清楚它和普通函数有什么区别。普通函数没有生命周期、没有state、不能被React追踪自定义Hook的命名必须以use开头并且在内部调用了其他hooks。这个命名约定不是规范建议而是React在编译和调试时识别Hook的约定lint插件会强制检查它。3. 渲染机制与性能优化面试必答的底层逻辑3.1 理解“重新渲染”到底发生在什么时候很多候选人被问到“setState之后发生了什么”会卡壳因为逻辑链条太长。我把它拆成几个节点面试时反而容易讲清楚setState触发当前组件进入更新流程React创建新的虚拟DOM树复用尽可能多的旧节点与上一次的虚拟DOM树进行diff计算出需要更新的最小范围React 18中并发特性可能让更新被中断、合并或优先调度最终批量提交变更到真实DOM注意React 18的automatic batching是默认开启的也就是说在Promise回调、setTimeout里连续调用多个setState也只会触发一次渲染。老版本React只在事件处理函数里批量更新。这个点面试官很喜欢作为“React 18的新特性”来问。再看“什么时候会导致组件重新渲染”props变化、state变化、context变化、父组件重新渲染。其中“父组件重新渲染”是隐藏最深的坑——父组件每次更新子组件的函数本身被重新创建子组件如果不做任何优化React.memo、useMemo、useCallback默认会跟着重新渲染。性能优化面试题的90%都围绕这个展开。3.2 React.memo、PureComponent与浅比较React.memo是一个高阶组件作用是对函数组件做props浅比较如果props的引用和值都没变化就直接复用上次的渲染结果。内部原理其实就一句把组件包在一个缓存层里每次更新时逐个比较新旧props全部相同就跳过渲染。但我必须说的是memo的应用场景是有前提的父组件频繁重新渲染、子组件渲染开销较大、子组件的props变化频率低。如果子组件本身就是轻量组件渲染开销几十微秒包memo可能反而因为比较开销增加性能损耗。任何一个优化工具都不是无脑用的。浅比较需要注意的坑是如果一个props是对象字面量比如Child config{{ name: 张三 }} /那父组件每次渲染时这个对象都是新引用React.memo的浅比较直接认为是变化了memo形同虚设。这也是为什么需要useMemo来缓存这个对象。类组件的对应方案是PureComponent它在shouldComponentUpdate里做默认的浅比较。但类组件时代因为state结构容易变得很深浅比较经常误判所以现在新代码基本都切到函数组件memo的方案。3.3 key的故事为什么不能用indexkey在diff中扮演的角色前面已经提过。这里补充几个面试中可能追问的细节如果key相同但组件类型不同React会重建组件不会复用如果key不同但组件类型相同React认为这是两个不同的节点也会重建key应该是稳定且唯一的同一层级内不能重复还有一种场景是“列表项顺序会变化”的拖拽排序组件。你如果用了index作key拖拽之后所有元素都会复用错位状态混乱。正确做法是给每个item一个唯一id让React知道它只是位置变了可以复用并移动真实DOM节点。面试官有时候会问“key能不能为undefined”技术上可以但不建议。undefined会被当作没有key处理diff时走默认的同位置对比逻辑。还有一题“key在props里能拿到吗”——拿不到React在传给组件的props里会过滤掉key这是key作为保留字段的设计。部分候选人写this.props.key拿不到任何值甚至不知道这个设计这里可以重点记一下。4. 状态管理与数据流从Redux Context到现代方案4.1 Redux核心单向数据流与不可变更新Redux在面试中的地位依然不可动摇尤其在问“状态管理选型”的时候。哪怕现在项目里用Zustand、Jotai的团队越来越多Redux的核心思想仍然是衡量候选人“是否理解全局状态管理本质”的标尺。Redux单向数据流可以浓缩成一句话视图层通过dispatch触发actionaction到达reducer后生成新的state新state被订阅的通知机制推送给所有监听者视图重新渲染。整个过程不可逆、可追踪、可预测。面试中必须答出的几个点store是唯一的持有整个应用的stateaction是一个普通对象必须带type字段reducer是纯函数输入旧state和action输出新state更新必须遵循immutable原则不能直接修改旧state要返回一个新对象我再补充一个高频追问“为什么reducer必须是纯函数”因为纯函数的特点是相同输入永远相同输出不产生副作用不修改外部状态。这保证了Redux的时间旅行调试、状态回溯、日志记录等功能是可能的。如果reducer里有随机数、日期、请求、DOM操作状态更新就没法预测调试工具也会失真。handwritten代码题“实现一个简易Redux”的示例我贴一下核心部分function createStore(reducer) { let state reducer(undefined, { type: INIT }) const listeners [] function getState() { return state } function dispatch(action) { state reducer(state, action) listeners.forEach(listener listener()) return action } function subscribe(listener) { listeners.push(listener) return function unsubscribe() { const index listeners.indexOf(listener) listeners.splice(index, 1) } } return { getState, dispatch, subscribe } }这段代码虽然简单但已经完整体现了Redux的核心闭环getState拿状态、dispatch触发更新、subscribe注册监听。很多候选人能把Redux API用得很熟练但手写这个mini实现就露怯。建议面试前一定自己敲一遍理解每一行的意义。4.2 Context与性能陷阱为什么很多人说Context不能乱用Context是React自带的跨层级传递方案适合主题切换、语言包、用户信息这类低频更新但全局共享的数据。它解决的问题是“prop drilling”——组件层级特别深时逐层传props既繁琐又容易遗漏。但Context有一个明显的性能陷阱Context value一旦变化所有消费这个Context的组件全部重新渲染不管它们是否只关心其中一部分数据。这个“全部重新渲染”的力度很粗不能像组件props那样精确控制某一个子组件。我面试中会问“如果你用Context存了一个user对象其中只有name字段被Header组件用了别的地方都不用那user.avatar变了会发生什么”答案是所有消费这个Context的组件即使没用到avatar全部重新渲染。这在大项目里就是隐患。解决思路有几种拆细分Context不同数据放不同Context里按需消费用useMemo缓存Context的value减少value引用变化次数用useSelector之类的第三方库比如Zustand替代Context做高频更新状态另外Redux和Context不是替代关系。Redux是状态管理库Context是React内置的依赖注入机制。Redux内部其实也用了Context来向组件树传递store实例。4.3 现代状态管理从Zustand到Jotai近两年面试越来越常问“如果不用Redux你会选什么”。我在项目里用Zustand比较多简单说下思路。Zustand的核心优势是API简洁、无需Provider嵌套、支持异步action、默认做了selector机制避免过度渲染。import { create } from zustand const useStore create(set ({ count: 0, increment: () set(state ({ count: state.count 1 })), fetchData: async (url) { const res await fetch(url) const data await res.json() set({ data }) } })) function Counter() { const count useStore(state state.count) const increment useStore(state state.increment) return button onClick{increment}{count}/button }这里useStore(state state.count)是关键只有selector选中的值变化时组件才重新渲染。它本质上比Context方案更精细。面试时可以对比说Context适合低频、全局、偏配置型数据Zustand适合高频、按需订阅的业务状态。Jotai则更激进把整个应用状态拆成一个一个atom粒度更细心智负担更低。适合需要精细依赖追踪的场景但团队新成员上手曲线比Zustand略陡一些。状态管理选型没有银弹核心是理解不同工具的设计哲学。5. 组件通信完整方案与受控组件5.1 组件通信的几大路径组件通信是React面试的基础题很多场景题都从这里延伸。我把常见场景按需整理成一张速查表通信场景推荐方案说明父子组件通信props 回调函数子组件把事件通过回调传给父组件子父组件通信父传回调函数给子子组件调用props.onXxx(data)兄弟组件通信状态提升到最近公共父组件父组件作为“数据中枢”跨层级组件通信Context解决prop drilling问题任意组件通信全局状态管理库Redux、Zustand等无关联组件一次性事件事件总线自定义EventEmitter不推荐调试困难我特别提醒一下事件总线方案比如用mitt发布订阅在React里是可以用的但会脱离React的数据流导致状态变化无法被React DevTools追踪。除非是极简单的非业务事件比如全屏切换、某个全局toast否则不推荐。场景题推荐背熟“状态提升”的经典例子两个输入框分别输入姓和名下面显示完整姓名。核心是把firstName和lastName都放在父组件state里两个子组件分别负责修改其中一个字段父组件调用子组件的回调更新state完整姓名作为props传给展示组件。5.2 受控组件与非受控组件一个被面试官反复问的点受控组件与非受控组件核心在于“form元素的值由谁控制”。受控组件把表单值和state绑定每次输入都触发setState更新非受控组件让DOM自己维护值React通过ref去取值。受控组件示例function Form() { const [value, setValue] useState() return ( input value{value} onChange{e setValue(e.target.value)} / ) }非受控组件示例function Form() { const inputRef useRef(null) const handleSubmit () { console.log(inputRef.current.value) } return input ref{inputRef} defaultValue默认值 / }面试中印象较深的一道题是“如果受控组件的value设置后onChange里不调用setState输入框还能输入吗”答案是不能。因为value始终是state的值不更新state输入框会被强制“拉回”到原值。这个题考察的是你是否真正理解“受控”二字的含义。实际项目里受控组件用得多因为它能让你对输入值做同步校验、格式处理、防抖搜索等。非受控组件适合表单重置、文件上传value不可控等场景。5.3 ref的前世今生从createRef到useRef再到forwardRefref在React里就像一个“逃生舱口”用于访问真实DOM节点或组件实例。类组件时代用React.createRef()函数组件用useRef跨层级传递用forwardRef还有useImperativeHandle暴露自定义方法。面试中常让手写一个“点击按钮自动聚焦输入框”的例子function AutoFocusInput() { const inputRef useRef(null) const focusInput () { inputRef.current?.focus() } return ( div input ref{inputRef} / button onClick{focusInput}聚焦/button /div ) }再深入一点“ref在函数组件和类组件上的区别”是易踩坑点函数组件默认不能给ref因为没有实例要配合forwardRef把ref转发到内部DOM节点。但如果函数组件本身想被父组件拿到ref就要用forwardRef包一层。useImperativeHandle是面试进阶题它允许你自定义暴露给父组件的“实例方法”隐藏内部实现的细节。比如const Child forwardRef((props, ref) { useImperativeHandle(ref, () ({ focusInput: () { inputRef.current.focus() } })) return input ref{inputRef} / })这时候父组件通过childRef.current.focusInput()调用子组件内部的方法同时看不到子组件内部的具体DOM。这种封装方式在UI组件库开发里非常常见。6. React 18/19新特性面试新趋势与延伸话题6.1 并发特性startTransition与useDeferredValueReact 18最大的变化是推出了“并发模式”虽然默认行为对开发者透明但两个API在面试中经常被提到startTransition和useDeferredValue。startTransition解决的是“低优先级更新”阻塞“高优先级更新”的问题。比如用户在输入框里打字搜索关键词变化会触发一个大数据列表的筛选渲染这个渲染可能比较耗时导致用户输入时感觉卡顿。如果筛选不是最紧急的可以把它包在transition里import { startTransition, useState } from react function Search() { const [keyword, setKeyword] useState() const [list, setList] useState([]) const handleChange (e) { const value e.target.value setKeyword(value) startTransition(() { // 标记为低优先级更新 const filtered filterData(value) setList(filtered) }) } }注意startTransition内部会执行它包裹的函数以保证更新被标记为“可中断”。React会优先处理输入框本身的更新保持输入流畅再处理列表更新允许被更高优先级任务打断。useDeferredValue则是同一个思想的Hooks版本适合处理“父组件渲染慢但子组件不慢”的场景const deferredKeyword useDeferredValue(keyword) const list useMemo(() filterData(deferredKeyword), [deferredKeyword])展示列表用deferredKeyword输入框直接用keyword这样输入实时更新列表渲染可以延迟到浏览器空闲再执行。面试时可以把它和“防抖”做对比防抖是人为延后执行useDeferredValue是React根据用户设备性能和当前任务负载动态决定何时执行。这才是并发模式的精髓。6.2 useLayoutEffect与useEffect执行时机的区别面试里容易被问到“useLayoutEffect和useEffect有什么区别”但很多人只是背“一个同步一个异步”讲不透应用场景。useEffect是异步执行的在浏览器完成渲染之后才触发useLayoutEffect是同步执行的在DOM变更之后、浏览器绘制之前触发。这就意味着如果你在useEffect里读取/修改DOM布局属性操作的是“已经绘制完成的画面”可能会产生一帧闪烁在useLayoutEffect里做同样的操作可以在绘制前完成用户感知不到变化。典型场景是需要根据DOM节点的宽度或位置计算并设置工具提示框的位置。用useEffect可能先弹错位置再调整闪一下用useLayoutEffect则可以在浏览器绘制前完成位置计算视觉上无缝。使用建议是默认用useEffect只有当你明确知道需要在绘制前同步读取/修改DOM时才切换为useLayoutEffect。服务端渲染时useLayoutEffect会告警因为它不能跑在服务端。6.3 React 19展望与Taro等跨端框架的React实践React 19在编译优化、Actions、Server Components等方向持续推进虽然现阶段项目里大规模使用还不多但面试中聊到“新特性”时会成为加分项。另外跨端开发是React系面试的另一大分支。Taro就是一个用React语法写小程序/H5的多端框架它的核心是把React组件编译到不同平台。用Taro时常见的坑包括不能用DOM API、需要遵守Taro的编译限制、CSS在部分平台不支持复杂选择器。而React Native那边常见的问题是启动白屏、原生模块桥接、Hermes引擎开启后的兼容性等。如果你的简历写了自己做过跨端项目面试官很容易追问这些细节。我给个建议不熟悉的项目经历不要写在简历上因为你可能被往死里追问到实现层面。7. 常见面试题速查与避坑指南7.1 高频问题与“面试官想听到的回答”整理几个面试中出现频率最高的React问题以及对应的回答思路Q1为什么使用React它和Vue的区别是什么不要只回答“社区大、生态好”。更好的思路是React的核心理念是“一切皆组件、数据驱动视图、单向数据流、函数式编程”它把UI抽象成纯函数的输入输出配合Hooks可以更灵活地组织逻辑。Vue更强调模板语法、响应式系统更适合中小项目快速开发React更适合大型复杂应用因为它的生态和架构更偏向工程化。Q2setState是同步还是异步这个问题一定要分版本答。React 18之前事件处理函数里是异步批量更新setTimeout、Promise回调里是同步的但实际也有批量行为React 18开始自动批处理全场景生效所有setState都会被合并然后统一更新。真正的“同步”感只出现在你期望立刻读取最新state的时候但React建议不要依赖这种读取而是通过effect去拿。Q3useEffect的依赖数组能不能省略省略的话每次渲染都会执行effect等于一个没有优化效果的“每次渲染后触发”回调。可以但大多数场景是性能浪费。面试时可以补充“如果我不想监听某个变化又不想每次渲染都执行可以考虑用ref做标记。”Q4React为什么需要key详细答法参考上文diff相关章节。核心是key帮助React在列表变化时识别哪些元素被新增、删除、修改利用key做精确复用避免不必要的重建。用不稳定的key比如index会破坏整个过程。Q5Fiber是什么Fiber是React 16之后引入的“链表式虚拟DOM”结构目的是让渲染过程可以拆分成一个个小任务配合调度器实现“可中断、可恢复、可优先级排序”的更新过程。回答时一定要提到“requestIdleCallback/并发调度”这个概念因为Fiber是并发特性的底层基础。7.2 手写代码题的常见套路与评分点React面试基本都会让手写一个小组件或Hook。我在实际面试中常见的有实现useDebounce考自定义Hook useEffect清理函数实现一个倒计时组件考useEffect定时器清理实现一个Tab切换组件考受控组件与状态切换实现一个搜索输入框带防抖和请求竞态处理考组合技能实现一个组件点击外部区域时关闭考ref useEffect评分点通常包括是否处理清理函数、是否处理竞态、是否考虑到组件卸载状态、代码是否能跑。以“点击外部区域关闭”为例一个合格答案应该像这样function useClickOutside(ref, onClose) { useEffect(() { const handleClick (e) { if (ref.current !ref.current.contains(e.target)) { onClose() } } document.addEventListener(mousedown, handleClick) return () document.removeEventListener(mousedown, handleClick) }, [ref, onClose]) }这套代码考察的点监听事件绑定在document上、用contains判断点击是否在目标区域外、清理函数移除监听、依赖数组完整。每个点都在考察候选人是否踩过真实的坑。7.3 我踩过最深的几次坑最后分享几个实际开发中踩过、也在面试中拿来当反例的坑希望能帮你提前避雷。第一个坑是“在useEffect里依赖了函数但没写全”。我把一个fetchData函数定义为组件内部函数useEffect里用它发请求但依赖数组没把函数加进去。结果第一次渲染时请求正常后续state变化导致fetchData重新创建effect却还在用旧闭包里的老函数一直发旧请求数据一直是旧的。排查了很久最后用useCallback包裹函数并加入依赖数组解决。第二个坑是“直接修改state对象属性”。有次做表格编辑功能我图省事写了state.list[index].name value然后setState({ list: state.list })。React的浅比较认为state引用没变组件完全不重新渲染页面毫无反应。后面统一改成不可变更新const newList state.list.map((item, i) i index ? { ...item, name: value } : item ) setState({ list: newList })第三个坑是“过度使用React.memo”。一个列表页里所有子组件都包了memo每个props还传了对象字面量结果浅比较每次都判定变化memo完全失效反而多了一层比较逻辑的开销。后来我把要传给子组件的对象用useMemo缓存memo才真正生效。React的面试准备说到底是“理解设计意图”的过程。网上能搜到几百道题但很多题之间是相互关联的理解了虚拟DOM就理解了为什么JSX不能写if理解了重新渲染机制就理解了为什么memo需要搭配useMemo理解了闭包就理解了hooks的依赖数组为什么不能乱写。把这条线串起来React的技术体系就通了。我在带人的时候常说一句话面试不是背题是把底层逻辑讲清楚。所有看似“八股”的问题背后都是设计者为了解决真实问题做的取舍。你能把“为什么”讲明白面试官自然知道你是有实战经验而不是刷题刷出来的。这份整理覆盖了React面试的核心框架面试前用一天时间把关键代码手敲一遍比刷十遍文档都管用。