当前位置: 首页 > news >正文

React面试核心知识点全解析:从虚拟DOM到Hooks原理与性能优化

前端面试只要往深里问,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操作”抽象成一个可复用的层。因为有了虚拟DOM,React才能跑在浏览器、服务端(SSR)、甚至React Native的宿主环境上。真实DOM不是React的直接依赖,而是“目标宿主”之一。这个抽象层的存在,才是虚拟DOM最大的价值。

diff算法的核心逻辑,一句话总结是“同层对比、类型优先、key辅助复用”。具体规则我在实战中验证过无数次:

  • 同层比较,不跨层级复用节点
  • 标签类型不同直接重建(比如div换成span)
  • key相同且类型相同的节点,走复用更新逻辑
  • 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 className="box">你好</div>

实际编译成:

const element = React.createElement('div', { className: 'box' }, '你好')

如果你用React 17+,babel会使用新的jsx runtime,会自动从react/jsx-runtime导入jsx函数,这时甚至可以不需要显式import React。但如果你还在字面上“React.createElement”,就要清楚知道这是老runtime的行为。

知道这个本质之后,很多“为什么”就顺理成章了:

  • 为什么JSX里不能用if语句?因为JSX只是表达式,不是语句,if没有返回值,没法嵌入到元素树里。所以你需要三元表达式、&&或立即执行函数。
  • 为什么class要写成className?因为JSX编译后是JS对象,“class”在JS里是保留字。
  • 为什么属性名要遵循camelCase?tab-index这类带连字符的属性名,在JS对象属性访问里要写成tabIndex

我在面试中常让候选人现场转换一段简单JSX,并追问“这个createElement返回的对象大概长什么样”。只有真正理解JSX编译结果的人,才能脱口而出——返回的是一个描述节点的plain object,包含typepropskeyref等字段。这也是后面说“虚拟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之后发生了什么”会卡壳,因为逻辑链条太长。我把它拆成几个节点,面试时反而容易讲清楚:

  1. setState触发当前组件进入更新流程
  2. React创建新的虚拟DOM树(复用尽可能多的旧节点)
  3. 与上一次的虚拟DOM树进行diff
  4. 计算出需要更新的最小范围
  5. React 18中,并发特性可能让更新被中断、合并或优先调度
  6. 最终批量提交变更到真实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的故事:为什么不能用index

key在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触发action,action到达reducer后生成新的state,新state被订阅的通知机制推送给所有监听者,视图重新渲染。整个过程不可逆、可追踪、可预测。

面试中必须答出的几个点:

  • store是唯一的,持有整个应用的state
  • action是一个普通对象,必须带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),否则不推荐。

场景题推荐背熟“状态提升”的经典例子:两个输入框,分别输入姓和名,下面显示完整姓名。核心是把firstNamelastName都放在父组件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再到forwardRef

ref在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与useDeferredValue

React 18最大的变化是推出了“并发模式”,虽然默认行为对开发者透明,但两个API在面试中经常被提到:startTransitionuseDeferredValue

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更适合大型复杂应用,因为它的生态和架构更偏向工程化。

Q2:setState是同步还是异步?

这个问题一定要分版本答。React 18之前,事件处理函数里是异步批量更新,setTimeout、Promise回调里是同步的(但实际也有批量行为);React 18开始,自动批处理全场景生效,所有setState都会被合并,然后统一更新。真正的“同步”感只出现在你期望立刻读取最新state的时候,但React建议不要依赖这种读取,而是通过effect去拿。

Q3:useEffect的依赖数组能不能省略?

省略的话,每次渲染都会执行effect,等于一个没有优化效果的“每次渲染后触发”回调。可以但大多数场景是性能浪费。面试时可以补充:“如果我不想监听某个变化,又不想每次渲染都执行,可以考虑用ref做标记。”

Q4:React为什么需要key?

详细答法参考上文diff相关章节。核心是:key帮助React在列表变化时识别哪些元素被新增、删除、修改,利用key做精确复用,避免不必要的重建。用不稳定的key(比如index)会破坏整个过程。

Q5:Fiber是什么?

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面试的核心框架,面试前用一天时间把关键代码手敲一遍,比刷十遍文档都管用。

http://www.cnnetsun.cn/news/4314721.html

相关文章:

  • 开源高可用IM社交应用全栈架构:从消息可靠投递到跨平台实现
  • Gemini Enterprise for Legal:企业级法律AI合同审查与合规实践指南
  • 上海携程前端社招面经:五轮面试全流程复盘与核心技术考点总结
  • 大厂面试全攻略:从简历优化到系统设计的进阶之路
  • PPG无创血压估算:从信号处理到CatBoost建模全流程
  • UG NX三维电气布线设计:从原理到实战的机电协同指南
  • 2015小米实习笔试回顾:基础题与手写代码的筛选逻辑
  • STM32H573 Secure Manager与TLS 1.3集成:HKDF回退方案实战
  • 程序员高考卷:一份覆盖算法、代码评审与隐写的工程实践自测题
  • YOLO OpenVINO 部署实操 | 推理提速3倍,NPU单帧 8.33ms
  • 后端面试实战复盘:技术面、项目深挖与临场策略全解析
  • docling 文档解析如何用 3 行代码跑通:PDF、DOCX 转 Markdown 并直接喂给 RAG
  • XGBoost时间序列预测实战:从特征工程到滚动预测
  • Windows下cuDNN与CUDA版本匹配安装指南
  • Memos 自托管笔记故障排查与部署配置完整指南:8 类常见问题一次讲透
  • Goose 桌面应用完整上手指南:从安装到跑通第一个任务
  • Cherry Studio:如何把多模型 AI 收进一个桌面窗口
  • 如何借助Remotion模板市场从零到出片:新手完整指南
  • 腾讯后端面试复盘:从算法到系统设计的实战经验与避坑指南
  • 字节前端二面实录:从并发控制到Vue3响应式的深度考察
  • PowerShell 安装失败?跨平台安装与验证 5 步避坑完整指南
  • TD-LTE前导检测:Zadoff-Chu序列与匹配滤波实现
  • 2025算法岗面试核心考点与实战攻略:从机器学习到大模型全解析
  • 信息学奥赛C++实战指南:从环境配置到算法精通的系统提升
  • PowerShell 快速入门指南:从启动到跑通第一个脚本
  • Cadence OrCAD CIS元件库深度解析与工程落地指南
  • LX Music 桌面版:免费聚合 6 大音乐源搜索的跨平台播放器完整指南
  • 7款降重会改坏论文吗?实测打分各有侧重(2026)
  • Jellyfin 媒体服务器快速部署指南:免费搭建你的私人影音中心
  • 京东春招技术岗笔试复盘:算法题型、八股范围与时间分配全解析