
写React这个题目我其实犹豫过一阵。市面上讲React的资料多到你看不完但多数要么是纯文档翻译要么是语法堆砌真正能把“为什么这么设计”讲明白的并不多。我自己的经历是从给老项目补bug开始接触React一开始连props和state都分不清后来啃源码、读官方文档、在公司项目里踩了无数坑才慢慢形成一套自己的理解框架。这篇博客就按我平时带新人的思路来写不追求面面俱到但保证每个概念都讲清楚“是什么、为什么、怎么用”尤其适合刚学完HTML/CSS/JavaScript、准备啃前端框架的朋友也适合那些React写了一两年但始终觉得哪里没通透的同学。1. 先想清楚React到底帮你解决了什么问题1.1 从“操作DOM”到“描述状态”的转变很多新手学React最容易被“声明式”“组件化”“虚拟DOM”这些词唬住其实回到最初的问题为什么需要React在React出现之前前端干活的主流方式是用jQuery这类库直接操作DOM。页面简单的时候没问题但一旦页面里同时存在多个联动模块——比如一个筛选条件变化图表、列表、统计数字都要跟着变——你就得手动找到每一个相关DOM节点再把最新数据填进去。逻辑一多代码很快就变成一团乱麻改一个地方不知道会影响哪些地方。React换了一个思路我不去管DOM怎么变我只声明“当前状态是什么”用户界面长什么样由状态唯一决定。状态变了React自动去更新界面。用一句话概括就是“UI f(state)”界面是状态的函数。这个转变是革命性的它让前端开发从“面向过程地操作节点”变成了“面向状态地描述结果”。1.2 学习React前需要补齐的基础我见过太多人直接上来就学React结果卡在JSX的语法上觉得好神奇看不懂。其实React本身并不难难的是ES6语法和JavaScript的函数式编程思想没跟上。建议你在正式写React之前先把这几块基础夯实箭头函数、解构赋值、模板字符串、扩展运算符数组的map、filter、reduce方法模块化import和export类的基本语法如果要看旧项目的话Promise与async/awaitReact生态里到处都在用你把这些搞熟了React学起来会顺畅很多。反过来如果基础不牢遇到报错也不知道是JavaScript的问题还是React的问题排查效率会非常低。2. React的核心机制逐个掰开讲2.1 JSX在JavaScript里写HTMLJSX是React最显眼的一张名片长得非常像HTMLconst element h1 classNametitleHello React/h1;注意这里变量名是element但中间写的是HTML标签。浏览器不认识这种语法需要靠Babel这类编译器把它转换成普通的JavaScript函数调用。上面这行JSX实际会变成const element React.createElement(h1, { className: title }, Hello React);理解了这层转换你就能明白为什么JSX里写class要改成className——因为它是JavaScript对象里的属性名而class是JavaScript的保留字。类似的还有for要写成htmlFortabindex要写成tabIndex本质都是因为我们在写JavaScript表达式而不是直接写HTML。JSX不是React独有但它和React绑定得最紧。在JSX里你可以用{}随时插入任意JavaScript表达式比如const name 张三; const element p你好{name}/p;这里的{name}会被求值并渲染成字符串。{}里能放的东西非常多可以是变量、三元表达式、函数调用但注意不能放if语句和for循环这种语句因为它们不返回值。2.2 组件把页面拆成一块块积木React的核心思想之一是组件化。组件你可以理解成一个函数接收参数props返回界面JSX。React里有两种写法函数组件function Welcome(props) { return h1你好{props.name}/h1; }class组件class Welcome extends React.Component { render() { return h1你好{this.props.name}/h1; } }现在的主流写法是函数组件加Hooksclass组件更多是在老项目里维护。但我建议新手还是过一眼class组件因为大量面试题和线上老项目都还在用。组件的好处是复用和隔离页面再复杂拆成一个个小组件之后每个组件只负责自己那一小块逻辑出了问题也好定位。我的一个习惯是一个组件超过200行就考虑拆分不是硬性规定但能让代码清爽很多。2.3 props与state数据流动的两条线在React里数据分为两种props和state。这是新手最容易混的一点我单独拎出来讲。props是父组件传给子组件的数据在子组件内部是只读的你不能直接修改props想改的话只能通知父组件去改。这种设计保证了数据的单向流动数据从顶层往下传修改动作从底层往上抛。state是组件自己内部管理的数据用useState来声明唯一修改方式是调用set函数function Counter() { const [count, setCount] useState(0); return ( button onClick{() setCount(count 1)} 点击了{count}次 /button ); }state一旦变化组件自动重新渲染界面随之更新。你可以把props比作“父组件给孩子的零花钱”孩子能花但不能自己印state比作“自己的小金库”想怎么花由自己决定。2.4 生命周期与Hooks从“挂载更新卸载”到“副作用执行”React 16.3以前class组件有三个经典生命周期阶段挂载期mounting、更新期updating、卸载期unmounting。那时写组件要小心翼翼地在componentDidMount里发请求、在componentDidUpdate里做比较、在componentWillUnmount里清理定时器稍不注意就会疯狂请求或者内存泄漏。React 16.8推出Hooks之后函数组件也能拥有状态和生命周期能力写法完全不一样了。hottest的莫过于useState和useEffect。useEffect可以理解为“副作用钩子”它替代了几乎全部生命周期方法但语义完全不同useEffect(() { // 这里相当于 componentDidMount componentDidUpdate fetchData(); return () { // 清理函数相当于 componentWillUnmount }; }, [dependencies]);第二个参数是关键。传空数组[]表示只在挂载时执行一次传具体依赖项表示依赖变化时才执行什么都不传表示每次渲染都执行。这里要特别提醒一个React的原理函数组件本身是一段函数体每次渲染都会重新执行整个函数。所以你要是问“为什么React每次都返回一个render函数”答案是函数组件本身就是render函数渲染一次就重新执行一次所谓的“返回render函数”本质是React在每次渲染时重新调用组件函数拿到的都是基于当前props和state计算出的最新结果。这也是React函数组件得以保持“UI f(state)”这个纯粹性的根基。2.5 虚拟DOM与FiberReact性能背后的两个关键设计虚拟DOMVirtual DOM是React早期对比其他框架的核心卖点。它的思路是直接操作真实DOM很贵那我先用JavaScript对象模拟一个DOM树每次数据变化时先对比新旧两棵虚拟DOM树找出差异diff再一次性对真实DOM做最小化更新。虚拟DOM的diff可不是全量比较React有一套启发式优化只在同层级比较不跨层级移动节点。正因为有这层抽象React才能跨平台——不仅能渲染到浏览器DOM还能渲染到移动端原生组件这就为后面提到React Native埋下了伏笔。Fiber则是React 16引入的底层架构重构。React以前在渲染大型组件树时是同步递归的一旦开始就停不下来如果组件数量特别多会阻塞主线程页面就会出现卡顿。Fiber把渲染任务拆成了一个个小单元每次执行完一个小单元就检查一下有没有更高优先级的任务插队有就先处理高优先级的。用一个不恰当的比喻以前是一口气把一整条流水线工人安排完现在是每干完一个工位就回头看看老板有没有新任务。这个机制让React能实现时间切片、并发渲染也为后面的useTransition这些API打下了基础。很多面试官喜欢问Fiber的作用是什么其实就是两个字可中断。让React的渲染过程从“不可中断的同步任务”变成了“可中断、可恢复、可优先级的异步任务”。3. 实操从零跑通一个React项目3.1 环境搭建用Vite五分钟跑起一个React项目现在官方推荐用Vite建React项目比传统的Create React App快出好几倍npm create vitelatest my-react-app -- --template react cd my-react-app npm install npm run dev执行完这几条命令浏览器打开终端提示的地址一个React应用就跑起来了。Vite起步快的原因主要是利用浏览器原生ES Module开发阶段不需要打包按需编译冷启动体验非常好。项目结构里最核心的是src/main.jsx和src/App.jsx。main.jsx是入口文件里面用createRoot把React组件挂载到HTML里的某个div上const root createRoot(document.getElementById(root)); root.render(App /);这个挂载过程很多人会忽略但白屏排查时第一个就要看它。如果root对应的元素在index.html里不存在应用什么都不显示。3.2 写一个真正的组件列表渲染、事件与状态我习惯用“待办列表”来入门React因为它同时涉及状态、事件、列表渲染、条件渲染这些核心知识点。下面这个例子几乎涵盖了你写业务组件会碰到的大部分场景import { useState } from react; function TodoList() { const [todos, setTodos] useState([{ id: 1, text: 学习React, done: false }]); const [input, setInput] useState(); function addTodo() { if (!input.trim()) return; setTodos([...todos, { id: Date.now(), text: input, done: false }]); setInput(); } function toggleTodo(id) { setTodos(todos.map(todo todo.id id ? { ...todo, done: !todo.done } : todo )); } return ( div h1待办列表/h1 input value{input} onChange{e setInput(e.target.value)} placeholder输入待办事项 / button onClick{addTodo}添加/button ul {todos.map(todo ( li key{todo.id} style{{ textDecoration: todo.done ? line-through : none }} onClick{() toggleTodo(todo.id)} {todo.text} /li ))} /ul /div ); }注意几个关键点onChange里是setInput(e.target.value)input是受控组件值由React状态控制用户输入能实时反映到state里列表渲染用map而且每一项必须有唯一的key否则React无法高效地对比更新控制台也会报warning修改数组时千万不要用push这种原地修改方法一定要创建一个新数组React靠引用比较来判断数据是否变化你用push改了原数组React可能根本感知不到。我还遇到过一种情况一开始学React的人会直接在onClick里写setTodos([...todos, { ... }])这样虽然能跑但逻辑一复杂就很难维护。还是建议把逻辑抽成单独的函数至少让代码读起来像一个“动作”而不是一堆状态更新的堆积。3.3 Hooks实战useEffect与useRef的典型用法在实际业务中useEffect最常用来发请求useEffect(() { let cancelled false; fetch(/api/user) .then(res res.json()) .then(data { if (!cancelled) setUser(data); }); return () { cancelled true; }; }, []);这个cancelled标志是防止组件卸载后还在setState导致的内存泄漏警告。React 18里虽然警告变化了但做清理依然是好习惯。useRef也是高频Hooks它的作用有两个一是保存一个跨渲染周期的可变值二是拿到DOM节点引用。很多人以为useRef只是用来拿DOM的其实它在定时器场景里特别好用const timerRef useRef(null); useEffect(() { timerRef.current setInterval(() { // 定时任务 }, 1000); return () clearInterval(timerRef.current); }, []);这里之所以要用useRef保存定时器id是因为定时器id只需要创建一次但清理函数需要在卸载时拿到它。如果用普通变量每次渲染都会重新创建清理时拿到的可能是旧值。4. 高频问题与排查技巧实录4.1 setState为什么看起来是“异步”的很多新手写代码会这样const [count, setCount] useState(0); console.log(count); // 0 setCount(count 1); console.log(count); // 还是0是不是bug这不是bug是React的批处理机制。在React 16和17里事件处理函数中的多次setState会被合并成一次更新为了减少不必要的渲染。React 18的createRoot更进一步几乎所有场景都会自动批处理包括Promise、setTimeout内部。理解这个机制在面试里很加分实际开发中遇到“状态改了但拿不到最新值”的问题也多半是没搞懂批处理。如果你真要在更新后立即读取最新值可以改用useEffect监听这个状态或者在setState的入参里用函数式更新setCount(prev prev 1);函数式更新能拿到上一次状态的最新值在连续多次更新时特别可靠。4.2 闭包陷阱为什么定时器里读取不到最新state这个问题我几乎每隔一段时间就会在群里看到一次。经典场景是const [count, setCount] useState(0); useEffect(() { const id setInterval(() { console.log(count); // 永远是0 setCount(count 1); // 永远是从0加1 }, 1000); return () clearInterval(id); }, []);原因在于useEffect的回调只在挂载时执行一次它捕获的是当时的count值也就是0之后每次定时器执行时count已经不是最新的但闭包记住的还是旧值。解决方法是把count加到依赖数组里让effect重新执行或者用setCount(prev prev 1)函数式更新。这里我推荐后者因为定时器场景里重建定时器会带来额外的开销和可能的重置问题。在React 里函数组件的每一次渲染都会形成一次独立的闭包这是理解Hooks的关键也是很多bug的来源。你在调试时遇到“明明state变了函数里读到的还是旧值”这类诡异问题时先往闭包陷阱上想。4.3 白屏问题排查清单React的“白屏”是面试和工作中都高频出现的词特别是React Native启动白屏。前端Web白屏的排查顺序我总结成一张清单打开浏览器开发者工具Console看有没有JavaScript报错比如Cannot read properties of null这种多半是找不到挂载节点。看index.html里挂载容器是否存在且与createRoot挂载的id是否一致。看网络面板JS、CSS文件有没有加载失败特别是部署到服务器上路径配错时资源404会导致拿不到chunk文件。看路由配置BrowserRouter直接部署到服务器时刷新会404需要服务器做history回退否则只有首页能打开。如果是打包后上线白屏先看构建日志里有没有报错再看是不是HTML里没有引入对应的脚本文件。React Native启动白屏的原因则更偏引擎层面JS引擎初始化、bundle包的加载和解析都需要时间在低端机上尤其明显。常见的优化手段包括缩减bundle体积、按需加载模块、启动时先渲染一个原生占位界面、首屏用静态数据代替异步请求等。网上很多人问“react navite 在安卓低端机很卡”除了启动白屏列表长页面卡顿多半是没有做虚拟列表优化、或者在JS线程里做了太多运算把图片缓存、数据预加载、列表分页这些基础功夫做到位体验会有质的提升。4.4 面试高频题React与Vue到底差在哪里React和Vue的对比几乎是每次面试必问。我的看法是两者都是优秀的组件化框架但设计哲学非常不一样。对比维度ReactVue核心思想UI f(state)单向数据流响应式数据绑定模板语法 自动依赖追踪UI描述方式JSXJavaScript表达式能力模板接近HTML有指令系统v-if/v-for状态变更不可变数据setState开启新渲染可直接修改data属性Vue内部响应式拦截渲染机制虚拟DOM Fiber调度虚拟DOM 细粒度依赖追踪结合学习曲线生态自由写法需要自己摸索规范模板上手快约定多小项目开发效率高生态Redux/Zustand/React Router等Vuex/Pinia/Vue Router等官方全家桶更齐全要我说选哪个更多取决于团队和场景。React的自由度高社区和岗位量大适合想在前端深耕的人Vue的上手体验更顺滑文档对中文用户友好中小项目落地快。别当成非此即彼的选择两个都值得了解。5. 从单一技能到生态体系React技术栈的扩展方向5.1 React Native用一套代码写跨平台AppReact NativeRN是React向移动端延伸的方案。它的原理说起来很“取巧”用React的组件模型描述界面运行在JavaScript引擎里通过一个Bridge新架构里是JSI把渲染指令传给原生系统最终渲染的是真正的原生UI组件不是WebView。相比H5套壳方案RN的体验更接近原生。但RN也不是银弹。启动白屏是它的老问题官方也在不断优化从原来的打包整个bundle到支持按需加载分拆bundle再到新架构的懒加载。如果你负责的App在Android低端机上卡顿我建议优先检查三点图片是否做过缩放和缓存、FlatList有没有设置getItemLayout和removeClippedSubviews、JS线程是否有大量同步计算。这三步做完卡顿至少能缓解大半。5.2 React图表库怎么选业务里“React 图表”搜索量一直不低说明大家都会遇到做数据可视化的需求。现在常见的方案有Recharts基于SVGAPI非常符合React直觉适合常见折线图、柱状图、饼图上手最快。ECharts echarts-for-reactECharts功能最全适合复杂数据可视化但它的核心理念是配置项不算React友好需要自己封装一层。visxAirbnb出品基于D3和React的组合灵活度高但上手成本也高。Ant Design Charts如果你在用Ant Design这个UI组件库它家的图表库是无缝衔接的。选型建议很简单如果只是普通后台统计图Recharts就够了如果是大屏可视化、地图下沉等复杂需求直接上ECharts生态别自己造轮子。5.3 Next.js和ViteReact到底什么关系热词里有关“next.js和vite react”的区别这个问题面试也非常爱问。很多人迷惑是因为两者都和质量体验有关其实它们完全是两个维度的东西。Vite是一个构建工具负责把源代码翻译、打包成浏览器能运行的文件React是一个UI库负责界面的渲染。Vite React是典型的前端SPA方案页面在浏览器端渲染首屏要等JS加载完才能展示内容对SEO不友好。Next.js则是一个全栈框架内置了React也内置了一套构建和渲染方案。它最大的特点是支持服务端渲染SSR和静态站点生成SSG。SSR的意思是服务器先把React组件渲染成HTML字符串返回给浏览器用户第一眼看到的就是完整内容不用等JS执行完。这对内容型网站、电商、需要SEO的应用特别关键。所以你如果做官网、博客、社区这类完全可以考虑Next.js做后台管理系统、内部工具这类ViteReact就足够轻快。6. 常见React面试题与避坑笔记6.1 必考的四类原理题我根据自己面试别人和被人面试的经验把React面试题归成四类可以说覆盖了大部分初级和中级岗位的需求第一类是“React是什么、为什么需要它”答案要围绕声明式UI、组件化、单向数据流来展开。第二类是“生命周期和Hooks”。class组件的三阶段要背熟同时理解useEffect对生命周期方法的替代关系这里最容易出新题比如useEffect依赖数组的坑、闭包陷阱、竞态问题。第三类是“组件通信”。父传子用props子传父用回调函数兄弟组件靠状态提升跨层级用Context或Redux/Zustand这类状态管理库。每个都要能举出例子。第四类是“性能优化”。核心是减少不必要的渲染方法有React.memo缓存组件、useCallback/useMemo缓存函数和计算结果、列表key尽量稳定、虚拟化长列表、避免内联对象或箭头函数作为props每次变化。6.2 跑React项目时的三个硬性规范最后分享三个我在实际项目中贯彻的硬性规范Hooks必须写在组件顶层不能放在条件语句或循环里。写成if (condition) { useState(...) }会直接改变每次渲染时Hook的调用顺序React会报错常见的就是Minified React error #130。这条规则几乎可以解释所有Hook相关的诡异报错。key不能用数组索引。用索引当key在列表头部插入数据时React会认为每个元素没变只是内容变了容易导致状态错乱。setState不要循环里调用。在循环里同步多次setState非常浪费渲染性能正确的做法是先计算好新状态再一次性setState。这三个规范里每一个背后都有我踩过的坑。Hooks条件调用能让人反复“白屏”key用索引的后果在动态表单里变得极其隐蔽删掉中间一行后面几行的内容串位了循环调setState的问题在数据量大时直接把页面卡死。你提前避开至少能少浪费一个通宵的调试时间。React这个技术栈我越用越觉得它不是一套“语法答案”而是一套“思维方式”。它在逼你关注数据从哪里来、到哪里去、什么时候变想清楚了这些React代码自然就写得顺了。这个内容后续还可以延伸到React Server Components、状态管理库的选型、组件库二次封装等多个方向每个都是独立的大专题。先把这篇文章里的基础啃透后面的路会越走越宽。