磨蹭了很久,终于开始系统性梳理React的学习笔记了。前端框架用了不少,但每次都是照着文档敲,敲完就忘,这次决定用写笔记的方式逼自己把原理层面的东西弄透。这篇笔记是第一阶段的内容,从组件化思想、JSX语法、状态管理到Hooks的基本用法,全部基于我实际敲代码时的理解整理,不是官方文档的复读。先说结论:React入门最难的不是语法,而是思维模式的转换。用惯了jQuery或者原生DOM操作的人,刚开始写React会觉得处处受限制——不能再随心所欲地操作DOM了,一切都要通过数据去驱动。但一旦跨过这道坎,你会发现组件化的开发方式让代码的复用和维护变得异常轻松。这篇笔记适合刚接触React、打算系统学习的前端开发者,也适合那些看过不少教程但总觉得没真正入门的初学者。1. 为什么我决定从组件化开始学React1.1 React到底解决了什么问题很多新手学React时会陷入一个误区:把React当成一个模板引擎来用。实际上,React解决的核心问题是UI状态同步。在没有React的年代,页面上的数据和界面是分离的,改个数据得手动去找对应的DOM节点更新,项目一复杂,代码就变成一团乱麻。React引入了一个核心思想:数据变了,界面自动跟着变。React管这套机制叫响应式渲染。它不直接操作DOM,而是维护一棵虚拟DOM树。当数据发生变化时,React先在虚拟DOM上做对比,找出真正发生变化的部分,再去更新真实的DOM。这个找不同的过程叫diff算法,它是React高效渲染的关键。对于前端新手,脑子里不需要装太多底层细节,但有一个概念必须根深蒂固:不要手动操作DOM。我刚学React时总忍不住想去document.getElementById,后来才明白,React已经把DOM操作封装好了,你只需要关心数据是什么样,剩下的交给框架。这就像你请了个保洁阿姨,就不要自己再去拖地了,你只管告诉阿姨厨房需要打扫,她会自己搞定。1.2 学习前需要具备的基础我不建议零基础直接学React,但你也没必要把JavaScript学到精通再开始。一个比较务实的标准是:HTML和CSS的基本用法要熟悉,JSX说到底还是HTML的变体ES6的常用语法要看得懂,尤其是箭头函数、解构赋值、模板字符串理解函数和对象的基本概念,知道什么是回调函数会使用npm或yarn安装依赖包重点强调的是第2条。React中和JavaScript的交互非常频繁,如果你连const [count, setCount] useState(0)都看不懂,那后面的学习会非常吃力。建议先花一周时间把ES6的常用语法过一遍,再投入React的学习。另外,我不建议一上来就学TypeScript版本。React本身的概念已经够多了,再加上类型系统会让新手抓不住重点。先把JavaScript版本跑通,再考虑引入TypeScript,这是比较平滑的学习路径。我当时就是先学的JS版,后面项目需要再补的TS,压力明显小很多。2. JSX语法与组件初体验:把HTML搬进JavaScript2.1 JSX到底是什么JSX是React最直观也最容易引起误解的部分。它看起来像HTML,但实际上是一个JavaScript的语法扩展。我第一次看到JSX时一脸疑惑:这不就是把HTML写在JS里吗?这算什么新东西?后来才明白,JSX的底层其实是函数调用。// 你写的JSX const element h1Hello, React/h1 // Babel编译后的JavaScript const element React.createElement(h1, null, Hello, React)看到了吗?JSX只是React.createElement这一函数调用的语法糖。这样设计的好处是,你可以像操作变量一样操作UI——把它塞进数组、当成参数传递、用条件语句控制它的输出。这是传统模板引擎做不到的,因为模板字符串只是单纯的文本替换,不具备逻辑能力。在写JSX时有几个细节需要注意:className代替class:因为在JavaScript中class是保留字,所以JSX中用className来定义样式类名事件用小驼峰:onclick要写成onClick,onchange要写成onChangestyle接受对象:style{{ color: red }},注意有两个花括号,外层是JSX表达式语法,内层是JavaScript对象还有一个新手最容易忽略的点:JSX表达式用单花括号{}包裹。在这个花括号里,你可以写任何JavaScript表达式,比如{1 2},{user.name},甚至{fn()}。但注意,只能写表达式,不能写语句。if语句、for循环都不能直接写在花括号里,这是JSX和Vue模板的一个重要区别。2.2 函数组件与类组件怎么选React组件有两种写法:函数组件和类组件。初学者经常纠结学哪个,我的建议是:直接学函数组件。这不仅是趋势,而且更符合JavaScript语言本身的风格。// 函数组件 function Welcome(props) { return h1Hello, {props.name}/h1 } // 类组件 class Welcome extends React.Component { render() { return h1Hello, {this.props.name}/h1 } }从代码量上看,函数组件明显更简洁。类组件需要理解this的指向问题,还需要记得render方法、constructor、super(props)这些固定的套路。函数组件就是一个普通的JavaScript函数,接收props,返回JSX,没那么多妖蛾子。尤其到了React 16.8以后,Hooks的出现让函数组件具备了和类组件一样的能力,甚至更强。现在新写的React项目,除非维护老代码,否则基本都用函数组件。如果你在教程里看到类组件的写法,可以看懂,但不建议深入模仿,直接学Hooks模式就够了。提示:一套代码里混用两种组件风格会让人崩溃。团队协作时,建议约定统一使用函数组件,这样代码风格一致,review时也轻松。2.3 Props传参:组件之间怎么说话组件之间通信最基础的方式就是props。props是父组件传给子组件的数据,子组件不能修改它,只能读取。这就像父母给孩子零花钱,钱给了孩子,但孩子不能自己印钞票。function UserCard({ name, age, avatar }) { return ( div classNameuser-card img src{avatar} alt{name} / p{name} ({age}岁)/p /div ) } // 使用组件 UserCard name张三 age25 avatar/img/avatar.png /注意我在函数组件的参数里直接使用了{ name, age, avatar }这种解构写法,这是props解构的惯用方式。这样可以避免每次写props.name这种冗长的形式。不过有一个细节要提醒:如果你给组件传了一个avatar属性,但忘记在组件里解构它,React也不会报错,这就导致了一些隐性bug。所以我习惯在组件顶部就把props解构好,一目了然。props还有一个经典的应用——children。它表示组件标签之间的内容:function Card({ children }) { return div classNamecard{children}/div } // 使用 Card p这是卡片里的一段文字/p /Card这种组合模式在设计通用组件时非常常用,比如弹窗、卡片、布局容器等。它体现了React的组件组合思想:不是通过继承来扩展,而是通过组合来复用。3. State与交互:让页面真正“活”起来3.1 useState为什么比手动改DOM好用props是外部传入的,state是组件内部自己管理的数据。如果说props是别人给你什么你就用什么,那state就是你自己口袋里揣了什么。useState是函数组件中管理state的入口,也是我学会的第一个Hook。import { useState } from react function Counter() { const [count, setCount] useState(0) const handleClick () { setCount(count 1) } return ( div p当前数字: {count}/p button onClick{handleClick}点击1/button /div ) }useState(0)返回一个数组,第一个元素是当前状态值,第二个元素是更新这个状态的函数。这种状态值 更新函数的组合,是Hooks设计的精髓。你不需要关心什么时候去更新DOM,只要调用setCount,React就会在合适的时机重新渲染这个组件,并自动更新界面。但新手在这里最容易踩坑:setState是异步的。比如你连续调用两次:const handleClick () { setCount(count 1) setCount(count 1) }你以为count会加2,但实际上只加1。因为React会批量处理同一个事件循环里的状态更新,第二次的setCount拿到的还是旧的count值。正确的做法是使用函数式更新:setCount(prev prev 1) setCount(prev prev 1)这样React会把前一次更新的结果传递给下一次计算,最终得到加2的效果。这个细节在官方文档里不太显眼,但实际开发中经常遇到,尤其是涉及多个连续更新时。3.2 生命周期思维:useEffect的使用场景类组件时代,React提供了componentDidMount、componentDidUpdate、componentWillUnmount等生命周期方法。函数组件中用useEffect来替代这些能力。但useEffect的思维方式不太一样,它不是让你在某个时刻做某事,而是让你在状态变化后做副作用。import { useEffect, useState } from react function UserProfile({ userId }) { const [user, setUser] useState(null) useEffect(() { // 当userId变化时,重新请求用户数据 fetch(/api/user/${userId}) .then(res res.json()) .then(data setUser(data)) }, [userId]) if (!user) return p加载中.../p return div{user.name}/div }第二个参数[userId]是依赖数组,它告诉React:只有当userId发生变化时,才重新执行这个effect。如果传入空数组[],effect只在组件挂载时执行一次,相当于componentDidMount;如果不传第二个参数,每次渲染都会执行,这通常不是我想要的。useEffect非常强大,但也容易被滥用。我自己就犯过一个经典错误:在useEffect里setState,又不小心把setState依赖的状态写进了依赖数组,结果造成无限循环。比如:useEffect(() { setCount(count 1) // count在依赖数组里,setCount又改了count }, [count]) // 无限循环这种依赖数组循环问题一旦出现,浏览器控制台会报Maximum update depth exceeded错误,页面直接卡死。排查方法是把依赖数组里多余的状态删掉,或者重新思考这个effect是不是真的有必要存在。很多时候,你根本不需要在effect里触发setState,而应该通过事件处理来完成。3.3 事件处理的几个坑React中的事件处理看似和HTML一样,但有几个坑非常隐蔽。第一个坑:this指向问题(主要影响类组件)。在类组件中,如果你直接给事件绑定一个方法,这个方法里的this不会指向组件的实例。需要手动绑定this.handleClick this.handleClick.bind(this)或者用箭头函数。但既然我推荐函数组件,这个问题基本可以不接触。第二个坑:事件处理函数的执行时机。新手常犯的错误是:button onClick{handleClick()}点击/button注意,这里是小括号,函数在渲染时就被调用了,而不是在点击时。正确的写法是onClick{handleClick},只传递函数引用。如果你需要传递参数,要用箭头函数包一层:onClick{() handleClick(id)}。第三个坑:事件冒泡与阻止默认行为。React的合成事件是通过事件委托挂载在根节点上的。如果你在多个嵌套组件里触发了事件,默认情况下事件会冒泡到父组件。想要阻止冒泡,需要在事件处理函数里调用event.stopPropagation()。想要阻止表单提交等默认行为,调用event.preventDefault()。注意React并没有完全遵循原生DOM的浏览器差异,这也导致不同浏览器之间事件行为可能不同,但React已经帮我们做了兼容处理,一般不用担心兼容性问题。4. 列表渲染与条件渲染:日常开发最频繁的操作4.1 key为什么不能省渲染列表在开发中太常见了,比如展示一个商品列表、一个好友列表,都涉及到数组的map操作。React中渲染列表需要在每个元素上加上key属性,这个key是用来帮助React识别哪些元素发生了变化的。function ProductList({ products }) { return ( ul {products.map(product ( li key{product.id}{product.name}/li ))} /ul ) }为什么不能直接用数组下标当key?因为当列表发生增删或排序时,下标会跟着变化,React会误以为每个元素都移动了位置,导致不必要的重新渲染,甚至可能出现状态错乱。比如你有一个输入框列表,用户在第一行输入了内容,结果你删除了第二行,由于key用的是下标,React可能把第一行的输入状态跟到了第二行,表现非常诡异。最理想的key是数据中稳定且唯一的字段,比如数据库的id。如果没有id,至少要保证它在列表中是唯一的,并且不会因为列表操作而改变。还要注意,key不是给开发人员用的,它只在React内部起作用。如果你尝试在组件内部用props.key去获取值,是获取不到的。React会拦截key作为特殊标识,不会把它传给组件。4.2 条件渲染的几种写法React中的条件渲染没有像Vue那样的v-if指令,而是在JSX内部用JavaScript的逻辑运算符来实现。最常用的三种写法:// 写法1: 三元表达式 {isLoggedIn ? Dashboard / : Login /} // 写法2: 逻辑与运算符(适合只渲染一个分支) {isLoggedIn Dashboard /} // 写法3: 立即执行函数(IIFE,不推荐,但有时很灵活) {(() { if (isLoggedIn) return Dashboard / if (isGuest) return GuestView / return Login / })()}三元表达式最常用,逻辑与运算符在满足条件才渲染一个元素的场景可以大幅缩短代码。但要注意,逻辑与运算符有个经典的坑:当左侧是0、空字符串等假值时,结果可能不是false,而是那个假值本身。比如:{count span有{count}条消息/span}当count为0时,页面不会显示有0条消息,而是直接显示0。这是因为0 something的结果是0,React会把这个数字直接渲染到页面上。解决办法是改成{count 0 span有{count}条消息/span}。我踩过这个坑,当时查了半天才发现页面上那个刺眼的0是怎么来的。后面就养成了一个习惯:用逻辑与运算符时,左侧一定要写成布尔表达式,而不是直接放一个可能为0或NaN的数值。4.3 优雅的列表更新策略列表更新是另一个让新手头疼的问题。比如你有一个待办事项列表,想要修改某一项的完成状态。很多新手会直接改数组的元素,但React要求你不能修改原数组,因为React依赖不可变数据来感知变化。// 错误示范:直接修改原数组 const handleToggle (id) { todos[id].completed !todos[id].completed setTodos(todos) // 没用,因为todos的引用没变,React不知道数据变了 } // 正确示范:创建一个新数组 const handleToggle (id) { setTodos(todos.map(todo todo.id id ? { ...todo, completed: !todo.completed } : todo )) }这个不可变数据概念,几乎是React的核心哲学。你每次更新state,都要生成一个新的对象或数组,而不是修改原来的。这样React才能通过浅比较Object.is来判断状态是否真的改变了。理解了这一点,后续学习Redux等状态管理库也会轻松很多。5. 从零搭一个React应用:完整实操记录5.1 项目初始化:脚手架工具的选择现在创建React项目的最快方式是使用Vite。虽然官方的Create React App依然可用,但Vite在开发体验和构建速度上全面胜出,而且官方文档也已经推荐新项目使用Vite。npm create vitelatest my-react-app -- --template react cd my-react-app npm install npm run dev执行完这三步,浏览器打开终端提示的地址,你就能看到默认的React模板页面。Vite的特色是开发服务器启动几乎无感知,还自带热更新,改代码后页面秒刷。我建议新手在搭建项目时不要一上来就往里面加各种依赖库。先只装React和ReactDOM,把基础跑通。在实际学习阶段,任何额外的库都可能成为干扰项,让你分不清这个概念是React的还是这个库的。5.2 第一个组件的编写与导入项目建好后,你会看到src/目录下有App.jsx和main.jsx。在React 17之后,新项目默认使用JSX,不再需要显式import React(旧版本需要)。main.jsx是整个应用的入口,它负责把App组件挂载到HTML中的root节点上。// main.jsx import ReactDOM from react-dom/client import App from ./App.jsx ReactDOM.createRoot(document.getElementById(root)).render( App / )我在写第一个组件时,习惯先在App.jsx里写一个简单的Hello World,确认渲染流程没问题,再逐步拆分组件。比如:// App.jsx function App() { return ( div h1我的第一个React应用/h1 p跑通了!/p /div ) } export default App这里有一个很多人忽略的点:export default 是ES模块的默认导出语法。如果你在学习别人的代码时看到export default App,而你自己写的是module.exports,就会报错。自ES6模块化普及后,前端各种工具对模块语法的要求已经统一为ES Module,建议顺手熟悉一下。5.3 状态提升:当两个组件需要共享数据按照我自己的学习节奏,第三个要体验的场景就是状态提升。假设你有两个组件,一个负责输入待办事项,一个负责显示待办列表。这两个组件之间需要共享一份数据,怎么办?做法是把这个共享的数据放到它们的共同父组件里,通过props传下去,这就是状态提升。function App() { const [todos, setTodos] useState([]) const addTodo (text) { setTodos([...todos, { id: Date.now(), text, completed: false }]) } return ( div TodoInput onAddTodo{addTodo} / TodoList todos{todos} / /div ) }这种设计看似简单,但它代表着React的数据流方向:自上而下。数据从父组件流向子组件,子组件想修改数据,只能通过调用父组件传下来的回调函数来间接实现。这种单向数据流是React的一个核心设计,它让应用的状态变化变得可预测、可追溯。如果两个兄弟组件需要通信,你就调整将它们的数据提升到它们的父组件来管理。注意:状态提升会带来prop drilling问题,即中间组件要传递与自身无关的props。当嵌套层级很深时,这个问题会很烦。后续可以用Context或状态管理库解决,但现阶段先把状态提升的思维练好,再考虑这些进阶方案。6. 初学阶段常见问题与排查实录6.1 组件怎么突然不更新了明明setState了,页面却不变是新手最常遇到的怪现象。我遇到的场景是:我在一个函数里setState,但是那个函数在组件外部定义了,导致闭包捕获不到最新的state。比如:function Counter() { const [count, setCount] useState(0) const handleClick () { setTimeout(() { setCount(count 1) // count是闭包捕获的旧值 }, 1000) } }这里setTimeout内部的count是函数创建时捕获的值,而不是最新的值。如果用户在1秒内点了多次,每次加的还是旧值。解决办法是使用函数式更新setCount(prev prev 1),这样每次都会基于最新值计算。还有一种组件不更新的原因是你在渲染期间直接修改了状态。比如在组件函数体内直接写setState(...),这样会触发render阶段更新的警告,React会拒绝这次更新。正确的做法是把setState放到事件处理、useEffect或异步回调中。6.2 控制台报错速查React在开发模式下对常见错误都有比较友好的提示,但很多新手看不懂。我把几个高频报错整理一下:报错信息含义解决方案Warning: Each child in a list should have a unique key prop列表缺少key给每个列表项加上唯一keyCannot read properties of undefined (reading map)数据初始化为undefined就调用了map初始化时给state一个空数组,或在地图前加判空Objects are not valid as a React child试图直接渲染一个对象渲染对象时要访问具体属性,或用JSON.stringifyToo many re-renders无限循环渲染检查useEffect依赖数组;检查是否在渲染阶段调用了setStateX is not defined变量名写错或忘记引入检查import和变量拼写其中Reading map这个报错尤其常见。很多接口返回的数据是异步的,初始化时可能是undefined,等到异步请求回来才有值。正确做法是useState([]),或者在渲染处使用可选链:data?.map(...)。6.3 新手一定要养成的三个调试习惯先说第一个:善用React Developer Tools。这是我学习React后最后悔没有早用的工具。安装这个浏览器插件后,你可以在开发者工具里直接查看每个组件的props和state。遇到这个值是怎么传进来的这种问题,打开React DevTools,点一下组件就能看到完整的props和state,排查速度翻倍。第二个习惯是学会在渲染函数里加日志。很多人调试组件时直接在事件处理函数里console.log,但忽略了组件函数的执行本身也是值得观察的。在组件函数顶部加一行console.log(render, count),你就能看到组件在何时被重渲染。如果发现它频繁渲染,就可以检查是哪个状态变化引起的。第三个习惯是遇到诡异问题,先怀疑自己的数据不是变了,而是引用没变。React的状态更新是基于引用对比的。比如你直接push一个元素到数组里,然后setState传这个数组,React会认为这个数组没有变化,因为引用还是同一个。所有修改都必须返回新引用。这几乎是React新手所有的灵异事件的根源。写这篇笔记的过程中,我重新把React的基础概念过了一遍,发现很多当初照抄能跑的代码,现在终于明白为什么这样写了。尤其JSX的本质、不可变数据、状态提升这三个点,是打通React任督二脉的关键。记笔记最大的好处,就是逼着你把模糊的概念落实到具体的代码和措辞上。接下来我打算继续整理Hooks进阶和自定义Hook的内容,后面还会写第二篇、第三篇,如果你也处在React入门阶段,可以一起交流踩坑心得。