React核心原理与实战总结:从Hooks到性能优化
发布时间:2026/9/29 17:10:43来源:尧图网络
最近带了一批实习生和应届生发现大家学 React 的路线出奇一致先啃官方文档再看几个视频教程最后写一个 TodoList 就觉得入门了。结果一到面试或者真正做项目连 useEffect 依赖数组为什么死循环都说不清楚。我干脆把自己这些年积累的 React 知识点全部翻出来按学生最容易理解的顺序整理成这份完全总结。这篇内容面向正在学 React、准备前端面试、或者刚开始参与项目开发的同学目标是让你看完之后既能写明白代码也能讲清楚原理。文章不会按官方文档的顺序平铺而是按“先理解设计、再掌握核心、最后对付实战”的路径来组织每个知识点都尽量说清楚为什么这么做。1. 先把React的底层逻辑盘清楚它到底解决什么问题1.1 为什么会有React从命令式到声明式很多学生一上来就背 JSX 语法、生命周期钩子学完还是写不明白。我建议先把 React 的设计初衷搞清楚——它解决的核心问题是“如何高效构建和更新用户界面”。在 React 出现之前前端主流方式是直接操作 DOM。比如要更新一个列表你得先找到容器节点清空里面内容再遍历数据逐个创建 DOM 元素塞进去。这套流程叫命令式编程每一步都要告诉浏览器怎么做。代码一多状态一变就要手动同步视图漏一步页面就出错维护起来非常痛苦。React 换了个思路你不需要关心“怎么把数据变成视图”只需要声明“数据在某个状态下界面应该长什么样”。UI 是 state 的函数UI f(state)。数据变了React 自己去对比新旧虚拟 DOM找出差异再精准更新真实 DOM这个过程叫协调reconciliation。这里可以打个生活化的比方命令式就像进后厨每一步都要告诉厨师“先放油、再放菜、加盐、翻炒”声明式就是直接点菜“来一份番茄炒蛋”怎么炒是厨房的事。React 就是那个厨房。理解了这层区别再去看 JSX、组件这些概念你会发现一切都是围绕“声明式”展开的。这也是为什么现在很多人用 AI 生成代码工具去写 React 时会觉得别扭——工具能帮你把 JSX 和样式码出来但如果你不理解状态和渲染之间的关系出了问题根本不知道从哪查起。框架可以换但“UI 是状态的函数”这种设计思想是通用的AI 时代反而更需要你把原理吃透。1.2 组件、元素与实例三个最易混淆的概念我面试时经常问“React element 和 component 有什么区别”能答清楚的人不多。这三个概念必须分清楚React element 是一个普通 JavaScript 对象描述你想在屏幕上看到什么。JSX 编译之后返回的就是 element。Component 是函数或类接收 props返回 React element。组件是“定义”是蓝图。Instance 是组件在运行时的具体存在。类组件有明确的实例this 指向它函数组件在 Hooks 时代用 hook 内部状态来承担实例的职责。举个例子Button /这种 JSX 写法本身既不是组件也不是实例它是在创建 element。真正调用组件函数生成 UI 的过程才产生“实例”的概念。理解这个区别最直接的用处看到React.createElement(type, props, children)时不懵。type 是字符串原生标签还是函数/类组件React 后续的处理路径完全不同。原生标签直接创建 DOM组件则要先执行组件代码拿到 element 再递归处理。这关系到虚拟 DOM 的构建和更新的性能表现面试问“React 是怎么渲染的”时能把这个链路讲清楚非常加分。2. JSX、Props与State绕不开的核心三件套2.1 JSX不是HTML编译之后的真相JSX 看起来像 HTML但它跟 HTML 有本质区别。JSX 是语法糖最终会被编译器Babel 或 esbuild转换成React.createElement调用。const element div classNameboxHello/div;编译后大致是const element React.createElement(div, { className: box }, Hello);所以你会看到几个“奇怪”的规则为什么用className而不是class因为class是 JavaScript 的保留字而 createElement 的第二个参数是一个普通对象属性名必须符合 JS 标识符规则。为什么事件用onClick驼峰写法因为 JSX 属性名统一走 DOM 事件的 camelCase 版本与原生 HTML 的小写写法区分。为什么{}里能写表达式因为 JSX 本质上是 JS 的世界{}里面放的是 JavaScript 表达式可以是变量、三元运算符、map 调用但不能是 if/for 这类语句。学生经常踩的坑是在 JSX 里写{if (condition) ...}。这是错的因为if是语句不是表达式。你只能用三元运算符或者把逻辑提出来放到函数体里先计算出结果再放进去。另外一个常见问题是 JSX 注释的写法{/* 注释内容 */}很多新手按 HTML 的!-- --写结果渲染出一堆乱码。2.2 Props与State数据从哪来归谁管Props 和 State 是 React 数据流的两个核心来源区别其实一句话就能说清props 是父组件传下来的数据组件内部不可修改是只读的state 是组件自己管理的数据可以通过 setter 函数修改。React 的数据流是单向的数据从父组件流向子组件通过 props 传递。子组件想改数据怎么办不能直接改 props而是通过父组件传下来的回调函数把“我要改”这件事通知父组件由父组件来改。这就是“状态提升”的由来——把多个组件共享的状态放到它们共同的父组件里管理。有个点学生常犯迷糊props 里的对象、数组子组件确实可以修改里面的某个属性因为引用没变React 不拦你。但这等于绕过了单向数据流违反 React 的设计原则会引起隐蔽的 bug。修改数据务必走 setter 函数保持状态的不可变性。记住一个原则组件对 props 只能读对 state 只能通过 setter 改明确了这条线数据流就不会乱。2.3 状态更新是异步的批量更新的坑这是面试高频题。“setState 到底是同步还是异步”答案是在 React 的事件处理和生命周期里是异步的在原生事件、setTimeout 等场景下React 18 之前是同步的18 之后引入自动批处理会让更多场景表现“看起来异步”。为什么设计成异步因为 React 为了性能会把同一事件循环里多次 setState 合并成一次更新。比如const [count, setCount] useState(0); const handleClick () { setCount(count 1); setCount(count 1); setCount(count 1); };连续调用三次setCount(count 1)最终 count 只会变成 1而不是 3。因为每次 setter 拿到的count都是旧值三次计算都基于 0。想要正确累加要用函数式更新setCount(prev prev 1); setCount(prev prev 1); setCount(prev prev 1);函数式更新接收上一次的 state 作为参数React 会把这些函数排队依次执行最终得到 3。这个细节在 React 18 并发模式下变得更加重要当渲染可能被中断时你必须基于最新的 state 来更新而不是基于闭包里捕获的旧值。说白了凡是“新值依赖旧值”的更新都建议用函数式写法从根上避开闭包陷阱。3. Hooks完整知识地图从useState到自定义Hook3.1 useState与useEffect最常用也最容易出错Hooks 是 React 16.8 引入的它让函数组件拥有了状态和副作用能力。函数组件没有 this所以一切靠 hook 在闭包里管理状态。useState 的要点有两个第一初始值可以是任意值也可以传一个函数做惰性初始化useState(() computeHeavy())这个函数只会在首次渲染时执行一次适合初始化开销大的场景第二修改状态时如果新值是对象或数组必须创建新的引用否则 React 认为“没变”不触发渲染。useEffect 则是另一个大坑聚集地。它用于处理副作用请求数据、订阅事件、操作 DOM、定时器。useEffect 的第二个参数是依赖数组useEffect(() { // 副作用逻辑 return () { // 清理逻辑组件卸载或依赖变化时执行 }; }, [dep]);三个容易出问题的点必须记住依赖数组不写每次渲染都会执行容易造成性能浪费依赖数组里传了对象或数组因为它们每次渲染都是新引用useEffect 会无限循环返回清理函数的场景事件监听、定时器、订阅一定要清理否则组件卸载后还会触发导致内存泄漏或“更新未挂载组件”的警告。提示写 useEffect 之前先问自己——这个副作用真的需要“跟随某个值变化”吗很多时候你可以把逻辑放进事件处理函数里或者用 useMemo 计算派生值而不是滥用 useEffect 来同步状态。React 官方也建议不要用 Effect 去根据其他状态调整状态这样写出来的代码经常出现重复渲染和时序问题。3.2 useMemo、useCallback与useRef性能与引用的三重门这三个 hook 作用各不相同但都跟“引用稳定性”有关。useMemo 缓存计算结果只有依赖变化时才重新计算。适合计算量大的派生数据比如从大数组中过滤出结果const filteredList useMemo(() { return bigList.filter(item item.name.includes(keyword)); }, [bigList, keyword]);useCallback 缓存函数引用返回一个稳定的函数。主要场景是传给子组件时避免子组件因为父组件每次渲染拿到新函数而被迫重渲染const handleDelete useCallback((id) { setList(prev prev.filter(item item.id ! id)); }, []);useRef 有两个用途存可变值修改不触发渲染和访问 DOM 节点。注意它和 state 的区别ref 改了一百次也不会导致页面更新适合放定时器 id、开关标志、上一次的 props 等。需要获取最新值时ref 是你的好帮手因为它不会像闭包那样捕获旧值。这里有一个非常重要的忠告useMemo 和 useCallback 不是“性能银弹”。滥用反而会让代码晦涩难懂。我的经验法则是——先想清楚是否真的存在性能问题再考虑优化。React 本身对少量重新渲染并不敏感官方也建议先用默认行为出了性能问题再做针对性分析。把时间花在组件拆分和数据结构上收益往往比堆 memo 大得多。3.3 自定义Hook把逻辑抽出来复用自定义 Hook 是 Hooks 时代最重要的复用方式它本质上是一个函数内部可以调用其他 hook函数名必须以use开头。一个典型的例子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 res.json()) .then(json { if (!ignore) setData(json); }) .catch(err { if (!ignore) setError(err); }) .finally(() { if (!ignore) setLoading(false); }); return () { ignore true; }; }, [url]); return { data, loading, error }; }注意里面ignore标志的用法当 url 变化或组件卸载时清理函数把 ignore 置为 true避免竞态条件下旧的响应覆盖新数据。这个细节十个学生里九个写不对但在真实项目里非常常见。比如搜索框快速输入时迟到的旧请求结果会把新结果显示出来又闪回去有了 ignore 标志就不会出现这种错乱。自定义 Hook 让逻辑可以在多个组件间共享比如 useCountDown、useLocalStorage、useDebounce都值得自己手写一遍。写多了你会发现它就是“把 useEffect 的复杂度封装起来”的手段。Hooks 有两条铁律只能在组件函数或自定义 Hook 里调用 hookhook 不能在循环、条件、嵌套函数里调用必须保持顶层调用顺序一致。原因很简单React 靠 hook 的调用顺序来记住每个 hook 对应的状态顺序一变就全乱了。4. 组件通信与状态管理别一上来就上Redux4.1 父子通信单向数据流怎么落地先理清最常见的场景父传子用 props 直接传子传父是父组件传给子组件一个回调函数子组件在合适的时机调用这个回调把数据作为参数传回去兄弟组件则把共享状态提升到最近的共同父组件父组件通过 props 把状态和修改函数分别传给两个子组件。状态提升是 React 最基础但也最重要的模式。举例一个搜索框和一个列表搜索词在搜索框里输入列表要根据搜索词过滤。正确做法是把 keyword 状态放在父组件中搜索框通过 props 接收 value 和 onChange列表通过 props 接收 keyword 进行过滤。这样数据的流向非常清晰输入变化 - 父组件 state 更新 - 列表重新渲染。很多学生一遇到跨组件传数据就想上全局状态管理其实大多数场景用“状态提升 组件组合”就能解决。还有一种技巧叫“children 透传”父组件不想逐层传递 props 时可以直接把子组件作为 children 传给中间组件让中间组件只负责布局数据由父组件直接管理。这个技巧能有效减少“props 层层钻透”的问题。4.2 Context跨层级通信与性能陷阱Context 解决的问题是“爷孙组件、甚至更深的组件之间传值”。它的使用方式const ThemeContext createContext(light); function App() { return ( ThemeContext.Provider value{theme} Toolbar / /ThemeContext.Provider ); } function Toolbar() { const theme useContext(ThemeContext); // ... }Context 的性能陷阱在于Provider 的 value 一旦变化所有消费这个 Context 的组件都会重新渲染哪怕它们只关心其中一部分数据。如果你把整个大的状态对象放进 value那任何一个小字段变化都会导致所有消费组件重渲染。实践建议有两点把关联紧密的数据合并成一个 context把不相关的拆分成多个 context减少无谓的渲染范围如果 value 里有函数用 useMemo 包住整个 value 对象避免父组件每次渲染都创建新引用导致消费组件全部重渲染。Context 不是不能用而是要用得克制别把全局状态一股脑全塞进去。4.3 状态管理库选型Redux Toolkit还是Zustand什么时候真的需要状态管理库我个人的判断标准是当你需要在完全不相关的组件之间共享大量全局状态比如用户登录信息、购物车、主题、权限并且状态的更新逻辑比较复杂时才考虑引入。选型建议Redux Toolkit 是官方推荐的 Redux 用法内置了 Immer简化不可变更新、自动创建 action、devtools 支持适合团队规模大、需要严格约束的工程。Zustand 是轻量方案API 简单不需要 Provider 包裹可以直接在组件外读写状态中小型项目体验极佳。如果只是简单场景useReducer Context 就够用了完全不必引库。给学生的建议是不管面试题怎么问都要能回答“Redux 解决的问题是什么”——可预测的状态管理。它强调单一数据源、状态只读、通过纯函数reducer修改。理解这三个原则比背 API 重要得多。面试时能说出“Redux 的 state 变化是可以通过 devtools 追踪的因为所有修改都经过 reducer而 reducer 是纯函数”这句话比背一堆 action type 有用多了。5. React 18/19 新特性面试问得越来越多5.1 并发模式与自动批处理React 18 引入了并发特性核心思想是“渲染可以被打断”。以前一次 setState 之后 React 会一次性同步更新完整个组件树中途不能停。并发模式下React 可以根据紧急程度安排更新紧急更新用户输入优先不紧急更新大数据渲染可以拖后、可以中断。对开发者影响最大的是自动批处理。React 18 之前只有 React 事件里的 setState 才会批量合并Promise、setTimeout、原生事件里的 setState 不会。18 之后所有地方都自动批量处理。这减少了很多“为什么在 setTimeout 里 setState 会多渲染两次”的疑惑但也要求大家更习惯函数式更新。你只要记住一句话不管在哪多个 setState 都会合并成一次渲染别指望中间能读到最新的值。5.2 useTransition与useDeferredValue处理“卡顿”并发特性给了两个非常实用的 hook 来解决“大列表渲染卡顿”的问题。useTransition 可以把某个更新的优先级调低const [isPending, startTransition] useTransition(); startTransition(() { setFilteredList(filterBigData(keyword)); });这样当用户输入时React 会优先响应输入事件等空闲了再去计算和渲染过滤后的列表。isPending 可以用于显示加载状态避免页面完全没有反馈。useDeferredValue 的作用类似它让你把一个值标记为“可以延迟”const deferredKeyword useDeferredValue(keyword); const list useMemo(() filter(list, deferredKeyword), [list, deferredKeyword]);注意点这些优化并不是万能药。如果过滤本身需要几百毫秒延迟渲染只是把“卡”推迟或分散真正要治本还得靠数据分页、虚拟滚动、Web Worker 等手段。面试里能说出它们的原理和适用边界绝对比背 API 加分得多。5.3 React 19 的Actions与use()React 19 在 2024 年底正式发布对表单处理和资源加载做了很多改进。对学生来说值得关注几个点Actions 在表单提交中使用 action 函数可以自动管理 pending 状态、乐观更新、错误处理以前要自己写一堆 loading 状态现在框架帮你处理了use()可以用于读取 Promise 或 Context它可以配合 Suspense 做数据获取比 useEffect fetch 更声明式ref 可以作为 props 直接传给函数组件不再强制要求 forwardRef。React 19 还默认支持meta、link等文档元信息标签的渲染优化配合框架时 SEO 更友好。对刚学的同学这些新特性不用急着全部掌握但至少要知道 React 还在持续演进。面试官如果问“你了解 React 19 吗”你能说出 Actions 和 use() 就算合格。学习新版本的正确姿势是先去官网看一遍“What‘s New”挑跟你当前项目相关的特性试试其余的知道存在即可。6. 性能优化从理论到能落地的清单6.1 减少不必要的重新渲染memo、PureComponent与组件拆分React 默认行为是父组件重新渲染子组件跟着重新渲染。React.memo 是干嘛的它让函数组件具备“props 没变就不重新渲染”的能力const MemoChild memo(Child);memo 只做浅比较如果你的 props 里有每次渲染都新建的对象或函数memo 就失效了。这时候 useCallback 和 useMemo 就派上用场了三者经常配合使用。但比 memo 更重要的是组件拆分。如果一个组件内部有“某个局部状态更新导致整个大组件重渲染”的问题应该把状态往下放或者把昂贵的部分拆成独立组件让它们自己管理自己的渲染。性能优化的第一原则永远是无损减少渲染范围而不是堆 memo。你宁可把一个组件拆成三个小组件也不要在每个组件外面都包一层 memo。拆完之后局部状态的变化只影响应该影响的部分这才是 React 性能优化的正确打开方式。6.2 key的diff原理与索引陷阱列表渲染时为什么要给每个元素一个稳定的 key因为 React 的 diff 算法靠 key 判断“哪些元素是同一个元素”。使用数组下标做 key 的问题在于当列表头部插入一项时后面所有元素的 key 全部变化React 会认为它们都是新元素被迫重建整个列表状态和 DOM 都会丢失。最典型的场景就是待办事项列表你在第一条前面插入新项后面所有输入框里的内容全丢了。解决办法是使用数据里唯一且稳定的 id。如果是临时拼出的列表且无增删操作用 index 勉强可行但你要清楚隐患在哪里。注意key 的“唯一”范围是兄弟元素之间不是全局唯一。同时避免用随机数Math.random做 key因为每次渲染 key 都变等于告诉 React“所有元素都是新的”性能直接崩盘。6.3 不可变数据更新与ImmerReact 判断状态是否变化靠的是引用比较。所以更新状态时一定要创建新的对象或数组不能原地修改// 错误原地修改 state.list.push(newItem); setState(state); // 正确返回新数组 setState({ ...state, list: [...state.list, newItem] });深层次嵌套数据用展开运算符写起来很痛苦所以有了 Immer 这种库它允许你“按可变的方式写但产出不可变的更新结果”import { produce } from immer; setState(produce(draft { draft.list.push(newItem); draft.user.name Tom; }));Redux Toolkit 内置了 Immer所以现在写 reducer 可以大胆使用“看起来是可变”的写法底层自动帮你生成不可变的更新。理解不可变更新的核心意义在于它让 React 可以高效地判断状态是否变化也方便做时间旅行调试——redux devtools 里能看到每一步的状态快照这在排查复杂 bug 时有奇效。你可以通过它查看某次操作前后 state 的完整变化过程再也不用靠 console.log 猜了。7. 实际场景实战TypeScript、图表、长连接与React Native7.1 React TypeScript类型定义的正确姿势现在几乎每个正规项目都在用 TypeScript 写 React。学生容易犯的错误是所有东西都用any等于没写类型。最基本的姿势type Props { title: string; onDelete: (id: number) void; children?: React.ReactNode; }; function Card({ title, onDelete, children }: Props) { // ... }useState 的类型推断通常能自动完成但如果初始值为 null需要显式声明const [user, setUser] useStateUser | null(null);useRef 在 TypeScript 下有一个经典坑useRefHTMLInputElement(null)返回的类型是RefObjectHTMLInputElement | null使用时需要判空。React 19 里 useRef 支持传入初始值参数类型体验更好了。总之类型定义的核心价值是约束接口让组件之间传参时的错误在编译期就暴露出来而不是运行时才炸。养成给 props 写类型的习惯对后续维护和团队协作帮助非常大。7.2 图表场景为什么选uPlot画K线如果你在做金融、量化类项目K 线图是绕不开的需求。React 生态里有 recharts、chart.js 等选择但如果数据量很大上万根 K 线、毫秒级 tick普通 SVG 图表库会卡。uPlot 是一个基于 Canvas 的高性能图表库体积小、渲染快比较适合 K 线图这种高频刷新场景。它的用法跟 React 不是天然结合的一般需要自己用 useEffect 初始化实例然后在数据更新时通过 setData 方法更新useEffect(() { const uplot new uPlot(opts, data, containerRef.current); return () uplot.destroy(); }, []); useEffect(() { uplotRef.current?.setData(newData); }, [newData]);选型的逻辑很简单先评估数据量和更新频率。一般业务报表用 recharts 就够了K 线这种重场景才值得上 uPlot 或 echarts 的 canvas 渲染模式。为了炫技盲目选重型库反而增加维护成本。实际项目里还要考虑图表的联动需求比如十字光标、缩放联动uPlot 的 API 相对底层封装成本是需要提前评估的。7.3 SSE与WebSocket实时推送文件变化怎么选在 React 项目里做实时更新有三种常见方案轮询用 setInterval 定时请求接口实现最简单但有延迟而且会在没有变化时浪费请求SSE 是单向推送服务器到客户端基于 HTTP自动断线重连适合“服务端文件变化通知客户端刷新列表”这种单向通知场景WebSocket 是双向通信适合聊天、实时协作等双方都要发消息的场景。React 侧接入 SSE 时注意在 useEffect 里建立连接并返回清理函数关闭连接防止组件卸载后连接泄漏useEffect(() { const es new EventSource(/api/files/updates); es.onmessage (e) setFiles(JSON.parse(e.data)); return () es.close(); }, []);判断依据很简单只需要服务端通知客户端用 SSE需要双向高频交互用 WebSocket变化频率低、可接受延迟用轮询。别一上来就整 WebSocket成本和复杂度高很多。另外提一句实际开发里常见的是“轮询文件变化”如果变化不频繁直接轮询一个带版本号的接口就够了不用上长连接。7.4 React Native白屏问题排查React Native 是 React 在移动端的延伸“启动白屏”确实是新手必踩的坑。白屏通常有几类原因开发模式下 bundle 没加载完成Metro 服务没启动、设备连不上、bundle URL 配置错误。检查 Metro 终端日志确认设备和控制台在同一网络。原生依赖没配置好引入的第三方 native module 需要 pod installiOS或重新构建Android漏了就会白屏或直接崩。JS 线程卡死主线程或 JS 线程执行了阻塞操作渲染不出来。大型数据同步处理、死循环都可能导致。启动时网络请求失败且无错误兜底页面依赖异步数据请求失败又没有 catch 处理界面就一直是空白。排查建议先看 Metro 和原生日志再看是否在真机上正确加载了 bundle最后复现路径。React Native 虽然这几年很多团队转向了别的跨端方案但作为 React 生态的一部分面试时能说出原理和排查思路还是很加分的。比如你能讲出“React Native 的渲染是通过 JS 线程把组件树转成原生视图的 shadow tree白屏往往意味着这个链路某一步断了”这就说明你真的理解底层了。7.5 AI时代React与其他框架到底差在哪现在 AI 热度高“react 框架和其他框架的区别”被频繁搜索。这类问题主要出现在技术选型和面试对比。React 与其他框架Vue、Svelte、Solid的核心差异说到底就是“更新原理”React 用虚拟 DOM 协调算法每次渲染从根组件开始对比出差异再更新真实 DOM配合不可变数据。Vue 用响应式依赖追踪 模板编译框架知道哪些组件依赖了哪些数据更新粒度更细。Svelte 在编译期做手脚直接把依赖关系编译成高效的命令式代码运行时几乎没有框架开销。Solid 则在运行时用细粒度的 signal连虚拟 DOM 都省了。那 React 的护城河是什么一是生态组件库、状态管理、路由、SSR 框架Next.js都极其成熟二是心智模型简单统一“UI f(state)” 一套思想贯穿 Web 和 React Native三是大厂背书和人才储备招聘需求量大。对学生来说框架之争不是重点。把 React 学扎实再横向对比了解 Vue 的响应式原理你会发现各种框架的思想互相借鉴核心能力是相通的。8. 学生最常见的坑与高频面试题速查8.1 useEffect依赖数组死循环死循环的经典写法useEffect(() { setCount(count 1); }, [count]); // count 变了又触发 effect无限循环排查思路问自己三个问题——effect 里是否调用了 setState是否触发了依赖值变化是否在渲染期间执行了副作用前两个中了基本就是死循环。另一个隐蔽版本是依赖数组里放了一个对象字面量dep{{ a: 1 }}每次渲染都是新对象effect 每次都会执行。解法是把对象提取成常量或者用 useMemo 稳定引用。实际项目中还有一种常见情况useEffect 里调用了某个函数而这个函数定义在组件内部导致它的引用每次渲染都变。解决办法是用 useCallback 包住函数或者把函数直接写在 effect 内部。经验是尽量让依赖数组保持简单别为了“把所有用到的变量都加上”而把数组写得越来越长那样只会制造更多触发点。8.2 setState的闭包陷阱这类题目在面试里几乎必考const [count, setCount] useState(0); useEffect(() { const timer setInterval(() { setCount(count 1); // count 永远是从第一次渲染捕获的 0 }, 1000); return () clearInterval(timer); }, []);这个定时器里的 count 永远是 0因为 effect 只执行一次闭包捕获了初始值。解法有两个一是把 count 加入依赖数组并重新设置定时器二是用函数式更新setCount(prev prev 1)。后者更优雅因为定时器不用重置。这个例子能很好地说明在 React 里不要依赖“读取外部变量的当前值”要么用函数式更新要么用 ref 存最新值。理解了闭包陷阱你对 “state 的快照特性” 会有更深的认识——每次渲染都有自己的 props 和 state 快照异步回调里拿到的都是本次渲染的值。8.3 手写React到底在考什么热词里有“手写 react”这已经成了面试环节里的保留项目。面试官不是真的让你几百行抄一个 React而是通过一个简化版考察你是否理解 createElement 和虚拟 DOM 结构是否理解 render 如何把虚拟 DOM 挂载到真实 DOM是否理解更新时 diff 的大致流程至少能讲出按类型比较、按 key 复用。我建议学生自己动手写一个极简版只有 render 和 useState能把 JSX 渲染出来、点击按钮能更新数字。写完之后再去看官方源码或者 mini-react 教程理解会快很多。这一步的价值在于它逼你把 React 从“黑盒”变成“白盒”。当你亲手实现了 useState 的闭包存储和 render 的挂载逻辑之后再遇到 “为什么 hook 不能写在条件里” 这类问题不用背答案自己就能推出来。8.4 高频面试题速查表问题核心回答要点setState 是同步还是异步React 事件内异步批量React 18 后自动批处理覆盖更多场景并发模式下渲染可中断useEffect 和 useLayoutEffect 区别useLayoutEffect 在 DOM 变更后同步执行会阻塞绘制测量布局、避免闪屏时用它为什么列表 key 不能用 indexdiff 靠 key 判断元素身份列表插入/删除会导致错位重建、状态丢失React.memo 什么时候失效props 里有每次渲染新建的对象/函数memo 只做浅比较受控组件和非受控组件区别受控value 由 state 控制并 onChange 更新非受控内部自己管理用 ref 取值React 18 并发模式解决了什么长任务渲染可以中断高优更新插队提升交互响应组件通信有哪几种方式props 传值、回调通知、状态提升、Context、状态管理库Hooks 为什么不能写在条件里React 依赖 hook 调用顺序维护状态条件变化会导致顺序错乱这张表值得打印出来面试前过一遍。但记住面试官追问细节时不会满足于背答案上面的每个回答背后都是本文前面讲过的原理理解了才能扛住追问。比如你说“key 不能用 index”面试官接着问“那如果列表是纯展示没有状态呢”这时候你能答出“纯展示场景用 index 问题不大因为不需要复用状态”说明你是真的懂而不是背的。带学生这几年我发现能把 React 学好的往往不是记性最好的而是愿意动手拆源码、写 demo、故意制造 bug 再排查的那批人。最后分享一个小技巧学完一个知识点不要急着往下学先用自己的话把它的“是什么、为什么、怎么用、有什么坑”讲给同伴听讲不出来的地方就是你的盲区。React 的知识点看起来多但核心就一句话理解 UI 是状态的函数剩下的都是围绕这句话展开的工具和经验。
网站建设高端定制企业官网