分享一个我自己带新人时常说的做法:学 React 之前,先别急着写组件,用十分钟搞清楚它到底是干什么的。很多学生把 JSX、useEffect、Redux 背得滚瓜烂熟,但一问“React 和 Vue 的区别”“为什么要有虚拟 DOM”,整个人就愣住了。这篇总结整理了我这些年带学生、做项目、被面试官盘问之后沉淀下来的 React 知识体系,适合正在入门、准备期末或者面试、以及学了一段时间但知识很散的人。重基础、讲原理、给经验,尽量不堆术语。
1. React 整体认知:先弄明白它解决什么问题
1.1 React 是什么:从“命令式 DOM”到“声明式 UI”
React 是 Meta(原 Facebook)开源的一个用于构建用户界面的 JavaScript 库。它不是 Vue、Angular 那种大而全的框架,它解决的核心问题只有一个:当数据变化时,如何高效、可预测地更新界面。
在 React 出现之前,前端写页面是典型的“命令式”:拿到数据后,手动找到 DOM 节点、修改它的内容或 class、再绑定事件……代码逻辑和数据是混在一起的。比如往一个列表里加一项,原生 JS 的套路是 createElement、appendChild、还要考虑清空旧节点避免重复。项目一大,这种操作 DOM 的代码就非常难维护,你永远不知道哪个函数改过哪个节点。
React 换了个思路:你只需要声明“UI 长什么样”,也就是把数据映射成界面结构,剩下的交给框架。这就是“声明式”。我用一个最简单的例子说明这种差异:
const [list, setList] = useState([]); // 当我 setList([...list, 新项]) 时,页面上的 <ul> 会自动多出一行React 内部通过组件化把界面拆成一个个独立单元,每个组件只管自己的数据和外观。组件可以复用、组合、嵌套,这让大型项目的维护成本大幅下降。学 React 的第一课应该记在心里:组件化、声明式、由数据驱动视图,这三件事是 React 的立身之本。顺便说一下开发环境:React 项目通常跑在 Node.js 环境下,用 npm 或 yarn 装依赖、用 Vite 启动开发服务器,所以学 React 之前最好先会 Node.js 的基本命令,比如npm install、npm run dev。
1.2 虚拟 DOM 与 Diff:React 凭什么能“自动更新”
React 不会在每次数据变化时直接操作浏览器 DOM,因为直接操作 DOM 是很昂贵的,重排、重绘、样式计算都很耗性能。React 的做法是在内存里维护一棵虚拟 DOM 树:它用一个普通的 JS 对象来描述真实 DOM,比如{ type: 'div', props: { className: 'box' }, children: [...] }。
当 state 变化时,React 会重新生成一棵新的虚拟 DOM 树,然后用 diff 算法把新旧两棵树进行对比,找出哪些节点真正变了,最后只把这部分最小更新同步到真实 DOM。这个过程叫协调(reconciliation)。
这里要澄清一个很多学生理解错的点:虚拟 DOM 并不一定比直接操作 DOM 快。在某些极端场景下,手写原生 DOM 操作可能更快。虚拟 DOM 的真正价值有两个:一是把 DOM 操作从业务代码里抽象出去,开发者不用关心怎么改 DOM,只关心数据是什么,这是工程效率的提升;二是 DOM 的抽象层带来了跨平台能力——同一套 React 代码可以渲染到浏览器,也可以渲染到原生移动端(React Native)或桌面应用。所以面试被问“为什么需要虚拟 DOM”时,别只说快,要提到跨平台、开发体验、批量更新这几个关键词。
1.3 React 与 Vue、Angular 的区别:面试第一问怎么答
React、Vue、Angular 有什么区别,是学生面试几乎必遇的题。我习惯用三个维度回答:设计思路、语法风格、生态定位。
- Angular 是一个完整的框架,自带路由、HTTP 客户端、依赖注入、表单校验等一整套东西,适合大型企业项目,但学习曲线陡峭,模板逻辑相对严格。
- Vue 是渐进式框架,模板语法写起来顺手,响应式系统自动追踪依赖,开发者上手快,中文社区活跃,中小型项目和内部系统用起来很舒服。
- React 更“小而专”:核心只解决 UI 渲染,状态管理、路由、请求方案等都需要你自己组装。好处是灵活、生态极其丰富,坏处是选择太多,新手容易迷茫。
再补充一个更细节的对比,面试官很喜欢追问:
| 维度 | React | Vue |
|---|---|---|
| 视图描述 | JSX,本质是 JS 表达式,逻辑灵活 | 模板语法 + 指令,接近 HTML,学习曲线平缓 |
| 数据更新机制 | 状态变化后从组件重新渲染,配合虚拟 DOM diff | 基于 Proxy 的响应式系统,精确追踪依赖 |
| 组件写法规格 | 函数组件 + Hooks / 类组件 | 选项式 / 组合式 API |
| 生态与就业 | 大厂使用多,生态庞大,TS 结合好 | 国内中小厂使用广,上手快 |
我的观点是:对初学者,React 和 Vue 选一个主攻就行,但原理层面的东西是相通的——组件化、数据驱动、生命周期、虚拟 DOM,这些概念在哪个框架里都存在。把 React 的底层逻辑学透,以后切到 Vue 的成本很低。这两年 AI 应用和 Agent 场景火起来,React 作为 UI 层的地位依然稳固,因为任何 AI 能力最终都要落到界面交互上,而这恰恰是 React 最擅长的领域。
2. 组件与 JSX:React 最基础的表达方式
2.1 JSX 不是模板:那些容易踩的语法坑
JSX 是 React 自己发明的语法,允许你在 JavaScript 文件里直接写类似 HTML 的标签。很多学生会以为 JSX 就是在 JS 里写 HTML,其实它不是模板引擎,它最终会被编译成React.createElement调用。
正因为 JSX 是 JS 的语法糖,所以它有几个看起来像 HTML、但其实是 JS 规则的细节,新手经常在这上面翻车:
- class 要写成 className:因为 class 在 JS 里是保留字。
- style 要传对象:
style={{ color: 'red', fontSize: '14px' }},CSS 属性名用驼峰。 - 花括号里面写表达式:
{name}、{1 + 2}、{condition && <span>显示</span>}都是合法的,但不能直接写 if 语句,要用三元表达式或者提前算好变量。 - 注释要写在花括号里:
{/* 这是注释 */}。 - 列表渲染必须加 key:key 帮助 React 识别哪些元素变化了,不要默认用数组 index 当 key。
一个典型的学生 Demo 会踩的坑是:想给一组数据渲染列表,结果忘了加 key,控制台警告一堆。其实原理很简单,React 用 key 来跟踪每个节点:当列表顺序变化或增删时,key 能让 React 判断哪些是同一个元素,避免把整个列表全部重建。这个知识点面试爱考,后面第 7 章会细说。
2.2 函数组件与类组件:从类组件时代到 Hooks 时代
React 早期只有类组件,因为必须用 class 才能拥有 state 和生命周期。类组件长这样:
class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { /* 挂载后 */ } componentDidUpdate(prevProps) { /* 更新后 */ } componentWillUnmount() { /* 卸载前 */ } render() { return ( <button onClick={() => this.setState({ count: this.state.count + 1 })}> {this.state.count} </button> ); } }类组件的烦恼是:this 指向容易出问题(必须用箭头函数或 bind)、逻辑分散在多个生命周期里、代码复用靠高阶组件和渲染 props,容易形成嵌套地狱。所以 React 16.8 推出 Hooks 之后,官方推荐优先写函数组件。函数组件接收 props,使用 Hooks 管理状态和副作用,代码更短、逻辑更内聚。
不过面试中还是要懂类组件的生命周期,因为老项目里大量存在。记住三大阶段即可:挂载(constructor 到 render 到 componentDidMount)、更新(render 到 componentDidUpdate)、卸载(componentWillUnmount)。在 componentDidMount 里发请求、在 componentWillUnmount 里清理定时器,这是老 React 开发的经典模式。
2.3 Props 与 State:数据的两个来源
组件的数据只有两个来源:父组件传进来的props和自己内部维护的state。
Props 是外部如何看这个组件,它是只读的,子组件不能修改父组件传来的 props,这保证了单向数据流。如果你的代码里出现了props.name = 'xx',说明设计错了。Props 可以传任何东西:基本类型、对象、数组、函数,甚至是 JSX 片段(children)。
State 是组件内部自己管理的数据。修改 state 必须用专门的更新函数(setState或useState的 setter),绝不允许直接改:state.count = 3这种写法 React 侦测不到,界面不会更新。state 的更新是异步且批量合并的,在同一个事件处理函数里连续多次 setState,React 会合并成一次更新去渲染。
另一个经典概念叫状态提升(lifting state up):当两个兄弟组件需要共享同一个数据时,正确的做法是把这个数据放到它们共同的父组件里,由父组件通过 props 传给两个子组件。这是 React 推崇的自顶向下数据流的核心,比刻意引入状态管理库要自然得多。
3. Hooks 完全指南:现代 React 的核心
3.1 useState 与 useEffect:最常用的两个钩子
Hooks 本质上是一组让函数组件拥有状态和执行副作用的能力。函数组件本来是纯函数的思维,Hooks 则给它注入了生命周期能力。
useState用来声明状态:
const [count, setCount] = useState(0);注意useState(0)里的 0 是初始值,只在首次渲染时生效;setCount是更新函数,它触发重新渲染。如果你要基于旧值更新,最好用函数式写法setCount(prev => prev + 1),这样即使多个更新排队,也不会因为闭包拿到过期值而出错。
useEffect用来执行副作用——数据请求、订阅、定时器、手动操作 DOM 都算副作用。它的依赖数组决定了 effect 什么时候执行:
// 每次渲染都执行(不推荐滥用) useEffect(() => {}); // 只挂载时执行一次(模拟 componentDidMount) useEffect(() => {}, []); // 依赖变化时执行 useEffect(() => {}, [userId]);依赖数组是 useEffect 的灵魂,也是新手最容易写错的地方。我见过太多同学在 effect 里用了某个变量,却忘记把它写进依赖数组,结果拿到的是旧值;也见过把依赖数组写满,导致每次渲染都触发请求。实时数据页面最常见的写法就是在 useEffect 里建立 WebSocket 或 SSE 连接,然后在清理函数里关闭连接,这个模式几乎是固定套路,面试问实时通信场景时可以直接讲出来。
3.2 useEffect 的闭包陷阱:老值从哪里来
闭包陷阱大概是 React Hooks 里最著名的坑,几乎每个做过项目的人都被坑过。先看这段代码:
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(count); // 这里拿到的永远是初始值 0 setCount(count + 1); // 一直卡在 1 }, 1000); return () => clearInterval(timer); }, []); }问题出在哪?依赖数组是空的,所以 effect 只在第一次渲染时执行一次。那次执行时,闭包捕获的count是初始值 0。之后 count 再变,这个 timer 内部的 count 也不会更新,永远闭着 0。解决方法有三种,按推荐程度排序:
- 用函数式更新:
setCount(c => c + 1),setter 能拿到最新值,不依赖闭包里的 count。 - 把依赖数组写全:把
count加进依赖,但这样会导致每次 count 变化都会重新创建定时器,得配合清理函数。 - 用 useRef 保存最新值:
countRef.current = count,定时器里读countRef.current。
我个人的经验是:只要在 effect 里用到状态,就先检查依赖数组是否包含它。ESLint 的exhaustive-deps规则强烈建议打开,它就是用来治这个毛病的。很多同学觉得这个规则烦人,其实它是在救你。
3.3 useContext 与 useReducer:轻量级全局状态方案
跨组件共享状态时,不一定马上上 Redux。Context 是 React 内置的依赖注入机制,可以在不经过中间组件的情况下把数据传到深层子组件。
const ThemeContext = createContext('light'); function App() { return ( <ThemeContext.Provider value="light"> <Toolbar /> </ThemeContext.Provider> ); } function Toolbar() { const theme = useContext(ThemeContext); return <div>当前主题:{theme}</div>; }useReducer是和 Redux 同思想的状态管理 Hook:把多个状态更新收敛成一个 reducer 函数,通过 dispatch 一个 action 来描述发生了什么,由 reducer 决定状态变成什么。适合状态逻辑复杂、多个状态有联动关系的场景。比如购物车,有加商品、减商品、清空、改数量等多个操作,用 useState 写会堆一大串 setState,用 useReducer 则清晰很多:
function cartReducer(state, action) { switch (action.type) { case 'ADD': return [...state, action.payload]; case 'REMOVE': return state.filter(item => item.id !== action.payload.id); default: return state; } } const [cart, dispatch] = useReducer(cartReducer, []);Context + useReducer 组合起来,能覆盖中小型应用的全局状态需求:用 useReducer 管理数据,用 useContext 把 dispatch 和 state 注入到组件树里。这是我做中小型项目时的默认方案,比裸 Context 的性能和可维护性都好。
3.4 useRef、useMemo、useCallback:三个容易混的钩子
这三个钩子的功能经常被搞混,分开讲:
useRef有两个用途:一是拿到真实 DOM 节点,比如聚焦输入框:inputRef.current.focus();二是保存一个不会让组件重新渲染的变量。定时器 id、上一次的值、各种可变引用都适合放 useRef。它的.current是可变对象,修改它不会触发渲染,所以不要用 useRef 去存会影响 UI 的数据。
useMemo缓存计算结果。它接收一个返回值的函数和依赖数组,只有当依赖变化时才重新计算:
const expensive = useMemo(() => filterList(list, keyword), [list, keyword]);适合做昂贵的计算,比如大数组过滤、排序、格式化。它和 useEffect 的共同点是都有依赖数组,区别是 useMemo 发生在 render 阶段,在它里面做有副作用的操作会出问题。
useCallback缓存函数引用。它的本质是 useMemo 的一个特例:useCallback(fn, deps)等价于useMemo(() => fn, deps)。目的是让传给子组件的函数保持引用稳定,配合 React.memo 避免子组件无意义的重渲染。
一句话总结三者的使用场景:useRef 存可变数据,useMemo 存计算值,useCallback 存函数引用。注意,不要为了性能优化而盲目给每个值套 useMemo 和 useCallback,滥用反而增加内存和代码复杂度。优化的前提是先确认存在性能问题。
4. 组件通信与状态管理:多组件协作的套路
4.1 四种常见通信场景:从就近到全局
React 组件通信可以按远近分成几档:
父子通信:父传子用 props,子传父用回调函数。这是最基本的形式。父组件把onChange={setValue}传给子组件,子组件在合适时机调用onChange(newValue),数据自然流向父组件。
兄弟通信:通过状态提升把共享数据放到最近的公共父组件。比如两个输入框需要联动校验,就把两个值都放在父组件的 state 里。
跨层级通信:用 Context。比如主题、语言、当前登录用户这种全局信息,用 Context 可以绕开中间层直接获取,避免“prop drilling”(逐层透传)造成的繁琐和无谓的重渲染。
任意组件通信:全局状态库(Redux、Zustand)或者事件总线。适合多个互不相干的组件共享同一份数据,比如购物车、全局通知、登录态。
选型原则我总结成一句话:优先用 props,其次状态提升,再考虑 Context,最后才上全局状态库。很多学生一上来就装 Redux,结果复杂度和维护成本反而成了负担。
4.2 Redux 核心思想:单一数据源与不可变更新
Redux 的核心思想非常简单,就三条:
- 单一数据源:整个应用的状态存在一个 store 里,是一个 JavaScript 对象树。
- state 是只读的:无法直接修改 state,必须通过 dispatch 一个 action 来描述发生了什么。
- 使用纯函数 reducer 执行修改:reducer 接收旧的 state 和 action,返回新的 state。
因为 reducer 是纯函数,它不能直接修改参数、不能有随机数、时间或网络请求等副作用。每个 action 经过 reducer 之后得到一份全新的 state,配合不可变数据让调试和回溯变得容易——这也正是 React 依赖引用变化来触发更新的前提。
现在主流的 Redux 用法是 Redux Toolkit(RTK),它简化了很多样板代码。你不需要再手写一堆 switch,用createSlice就能同时生成 action 和 reducer。学生阶段至少要知道:Redux 适合全局复杂状态,不适合所有项目。如果只是几个组件共享一个状态,用 Context + useReducer 是完全够的,不用引入额外依赖。
4.3 轻量方案:Zustand、Jotai 什么时候用
这几年 Zustand 在 React 社区很火,因为它 API 极简、不需要 Provider 包裹、可以直接在组件外读写 store,TypeScript 支持也顺手。一个计数器 store 大约十行代码:
import { create } from 'zustand'; export const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }));选择状态管理方案时,我一般这样判断:
- 只有父子或兄弟通信:用 props 或状态提升。
- 少量跨层级数据:Context + useReducer。
- 全局复杂状态、多模块共享、需要时间旅行调试:Redux Toolkit。
- 想要轻量、现代、TS 友好:Zustand 或 Jotai。
这个问题的本质不是哪个库更好,而是你的项目复杂度到哪个程度。面试时这样回答“什么时候用 Redux”,会比单纯背概念得分高。
5. React 与 TypeScript:让代码更靠谱
5.1 为什么建议学生从一开始就上 TypeScript
现在大厂的 React 项目基本默认 TypeScript,面试也大概率会问到 React + TS。TypeScript 用类型系统约束变量和函数,最大的价值不是消灭 bug,而是让代码意图变得明确:组件接收哪些 props、每个字段是什么类型,看一眼接口定义就懂,编辑器还能提供自动补全和重构支持。
对学生来说,TS 还有一个隐性好处:它逼着你把数据结构想清楚。很多人写 React 报错是因为数据形状不明确——接口返回的字段到底是字符串还是数字?要不要可选?用 TS 把这些定义出来,很多潜在 bug 在写代码阶段就暴露了。
学习上建议顺序:先掌握 TS 基础(interface、type、泛型、联合类型),再学 React 场景下的常见写法。不要一开始就把 TS 学成“反人类语法”,实际项目里常用的知识点其实就那几个。
5.2 组件常用类型写法:一套可以直接抄的模板
这是我在项目里常用的几种类型写法,学生可以直接当模板用:
// 定义 Props 类型 interface ButtonProps { label: string; onClick: () => void; variant?: 'primary' | 'secondary'; children?: React.ReactNode; } function Button({ label, onClick, variant = 'primary', children }: ButtonProps) { return ( <button className={variant} onClick={onClick}> {label} {children} </button> ); }// useState 的泛型 const [list, setList] = useState<User[]>([]); const [name, setName] = useState<string>(''); // useRef 拿 DOM const inputRef = useRef<HTMLInputElement>(null); inputRef.current?.focus(); // 事件对象类型 function handleChange(e: React.ChangeEvent<HTMLInputElement>) { console.log(e.target.value); } // reducer 的 action 类型 type Action = | { type: 'INCREASE' } | { type: 'SET_COUNT'; payload: number };写熟这份模板后,你会发现“报错”从一种玄学变成了可以推理的判断。这里要补充一个细节:React.ReactNode表示任何可以被 React 渲染的内容,是所有 children 类型的第一选择,比any安全得多。
6. 性能优化:让 React 应用更快
6.1 React.memo + useCallback:阻止无意义的重渲染
React 的渲染机制是:父组件重渲染时,默认所有子组件都会跟着重渲染。这里的重渲染不一定代表 DOM 变了,但函数组件内部的代码会重新执行一遍。如果一个子组件渲染成本高,或者父组件频繁更新,就会造成无谓的性能损耗。
React.memo是一个高阶组件,它会对 props 做浅比较:如果 props 没有变化,子组件就跳过重渲染:
const ExpensiveChild = memo(function ExpensiveChild({ data }) { // 只有 data 引用变化时才执行 return <div>{data.join(', ')}</div>; });但注意,memo 只比较 props 的引用。如果父组件每次渲染都创建一个新数组或新函数传给子组件,即使内容一样,引用也都变了,memo 依然会认为 props 变了。这时候要用useMemo去稳定数组的引用,用useCallback去稳定函数的引用。三者是铁三角,配合使用才能真正发挥作用。
我的建议是:不要过早优化。先正常开发,遇到明显的卡顿,再用 React DevTools 的 Profiler 找出渲染频繁的组件,用 memo、useMemo、useCallback 精准优化,而不是给所有组件无脑包 memo——过度 memo 同样会消耗比较开销。
6.2 长列表渲染:虚拟滚动的原理
列表渲染是 React 项目最常见的场景,也是性能问题的重灾区。渲染一个 1000 条数据的列表可能还感觉不出来,但如果是 10 万条,就会明显卡顿,因为每条数据都是一个真实 DOM 节点,浏览器处理不过来。
虚拟滚动的原理是:只渲染当前视口内可见的那一部分条目,比如视口高度能显示 20 条,那么不管数据是 1 万还是 10 万,DOM 里始终只存在 20 个左右的节点。当用户滚动时,动态替换显示的数据项。
实际项目可以直接用react-window或react-virtualized这类库,几行代码就能接入:
import { FixedSizeList as List } from 'react-window'; <List height={600} itemCount={10000} itemSize={50} width="100%"> {({ index, style }) => <div style={style}>第 {index} 项</div>} </List>这里要理解itemSize这个参数:它告诉虚拟列表每个条目的固定高度,是计算滚动位置和可视区间的关键。如果你的列表项高度不固定,就要用动态高度的方案,复杂度会上升不少。所以产品设计时,如果能保持列表项统一高度,会给前端省很多事。
6.3 代码分割与懒加载:首屏加载更快
打包工具(Vite 或 Webpack)默认会把整个应用打到一个 bundle 里,首屏要下载全部代码,这在项目大起来以后是灾难。代码分割可以把不同路由对应的代码拆成独立 chunk,按需加载。
React 官方提供的两件套是React.lazy+Suspense:
const Home = React.lazy(() => import('./pages/Home')); const About = React.lazy(() => import('./pages/About')); function App() { return ( <Suspense fallback={<div>页面加载中...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </Suspense> ); }用户访问首页时只下载 Home 的代码,访问关于页时再下载 About。首屏体积小了,加载自然变快。面试被问首屏优化时,除了代码分割,还常提到这些:路由懒加载、图片懒加载、压缩静态资源、减少 DOM 层级、避免大包依赖。把这些按网络层面、构建层面、运行时层面分好类,回答会很有条理。
7. 生态与面试高频考点
7.1 React Router 与 Next.js:路由与工程化选型
React 本身不管路由,通常用 React Router 或 Next.js。React Router 是客户端路由库,核心 API 就几个:BrowserRouter、Routes、Route、Link、useNavigate、useParams。路由参数变化时,组件可以读取useParams拿到动态段,比如/user/:id里的 id;用useNavigate做编程式跳转:
// 路由定义 <Route path="/user/:id" element={<UserDetail />} /> // 组件里取参数 function UserDetail() { const { id } = useParams(); const navigate = useNavigate(); return <button onClick={() => navigate('/')}>返回首页</button>; }Next.js是目前 React 生态里最流行的全栈框架。它把 React 组件提升到框架层面,自带文件路由、服务端渲染(SSR)、静态站点生成(SSG)、API 路由。面试问 React 和 Next.js 的区别时,核心答法是:React 是库,解决 UI 渲染;Next.js 是基于 React 的框架,解决整个应用的工程问题,特别是 SEO 和首屏速度。
另外不少同学会问到 React Native。它是 React 在移动端的技术方案,用同一套声明式写法渲染原生组件。启动白屏常见原因是加载 JS bundle 耗时、首帧数据未就绪,优化方向通常是拆分 bundle、开启 Hermes 引擎、把初始化数据提前注入,以及做好启动屏到首帧的过渡。如果只想应付面试,知道这些方向就够了。
React 里做图表也是常见需求。轻量场景直接用 ECharts 的封装或者react-chartjs-2,如果对性能要求很高,比如做 K 线图,可以看看 uPlot,它的渲染机制极其轻量,数据量大的时候优势非常明显。
7.2 合成事件与 Fiber:面试加分项
合成事件:React 并没有直接使用浏览器原生事件,而是做了一层包装。所有事件都绑定到根容器上,通过事件委托来分发。这样做的好处是跨浏览器行为统一,比如onChange在不同浏览器上的表现被抹平了;还能按需分配内存,不至于每个组件都绑一堆原生监听器。如果你在事件处理函数里需要访问原生事件对象,可以通过e.nativeEvent拿到。
Fiber是 React 16 引入的底层调度架构。在老的 Stack 架构里,更新过程是同步递归的,一旦开始就不能中断,遇到复杂的组件树会阻塞主线程,导致页面卡顿。Fiber 把渲染工作拆成一个个小任务单元,赋予不同优先级,可以中断、可以恢复、可以丢弃低优先级任务,让 React 能够在浏览器帧之间见缝插针地执行更新,从而保证交互流畅。学生了解到这里就足够应付大部分面试题,再深入就是源码级别的 diff 和 schedule 细节了。
7.3 高频面试题与手写题速览
最后整理一批我在学生面试中经常见的高频题,附上核心答题思路:
- 为什么调用 setState 后不能立刻拿到新值?因为更新是异步的,React 会把多次 setState 合并到一次渲染中。如果确实要基于新值做操作,用 useEffect 监听该 state,或者在事件处理函数里用函数式更新。
- 为什么 Hooks 不能在循环、条件语句中调用?Hooks 内部依赖调用顺序来建立状态和 effect 的对应关系,每次渲染必须以完全相同的顺序调用 Hooks。条件调用会打乱顺序,导致状态错乱。本质上是 Hooks 用了链表或数组下标这种实现方式,不是 Map 数据结构。
- key 的作用是什么?key 是列表元素的唯一标识,帮助 React 在 diff 时判断节点是新增还是移动。不要用 index 当 key,否则在列表头部插入或删除时,React 会误判所有节点的身份,造成状态错乱或重复渲染。
- 受控组件与非受控组件:受控组件的值由 state 控制,每个输入都会触发 setState,再回填到输入框,表单数据与 UI 完全同步;非受控组件通过 ref 直接读取 DOM 值。受控组件更可预测,是 React 推荐的默认方式。
- 手写一个简易 useState:用闭包和数组下标模拟。每次渲染会重新执行组件函数,Hooks 从全局存储里读取上一次的状态,再返回当前值和新 setter。能写出这个思路说明你真的理解了 Hooks 的运行机制。
let hookIndex = 0; const stateList = []; function useState(initial) { const currentIndex = hookIndex; stateList[currentIndex] = stateList[currentIndex] ?? initial; function setState(newValue) { stateList[currentIndex] = newValue; render(); } hookIndex++; return [stateList[currentIndex], setState]; }这段代码不能直接用于生产,但它是理解 Hooks 工作原理很好的起点:为什么顺序不能乱?因为每个 hook 在 stateList 里的位置是按调用顺序分配的;为什么状态能跨渲染保留?因为 stateList 是组件渲染之外的全局存储。
最后再分享一个我自己的体会:React 的知识体系看起来庞大——Hooks、路由、状态管理、TS、性能优化、Fiber——但核心永远是一条主线:数据驱动视图。面试官问的花样再多,考的都是你对这条主线的理解有多深。学生阶段最好的学习方式不是背 API,而是批量做小实验:自己写一个计数器、做一个 todoList、再模拟请求拉数据渲染列表,把这些基础动作做熟,再往上叠加状态管理和工程化。
如果你现在正处在“看了很多教程但还是记不住”的阶段,我的建议是:把每个知识点压缩成自己的话写一遍,配合一个最小可运行的 Demo 跑一遍。写不出来就说明没懂,跑不出来就说明没会。React 没有捷径,但它也不是玄学,把每个概念背后的为什么想清楚,积累到一定量后你会突然发现整个体系都通了。