1. 为什么“rea”最后落到了 React 上1.1 三个框架摆在一起我为什么选了 React在搜索框里敲下“rea”三个字母的人大概率想找的是 React。我当年入坑前端时也一样在 Angular、Vue、React 之间来回横跳最后稳定用在了 React 这套生态上一写就是好几年。回头看这个选择其实是技术诉求和团队条件共同决定的不是单纯因为哪家社区看上去热闹。Angular 全家桶的完整度高但它的学习曲线偏陡对项目结构的约束也很强。Vue 的上手体验确实好模板语法语文感很强中小型业务推进非常快。而 React 走的是另一条路它不给你一套大而全的规范而是把所有能力表达成“组件 状态 渲染”的组合。组件可以只有一层函数状态可以用一个 Hook 提起渲染则是状态的纯函数映射。这套模型看起来原始但对“复杂交互型产品”反而非常顺手因为你能清晰地控制数据什么时候变、哪个视图跟着动。团队方面也很关键。我所在的团队要维护一套长期迭代的 mid-size 业务系统人员会有流动。React 的生态资料和社区问答量级最大新人进来以后搜问题普遍不会卡死在某个小众点上。选型不只是技术领先度的问题更是团队能不能长期低成本维护的问题。衡量之下React 成了那个“大概率不会错”的选项。1.2 组件化思维的真正价值在“复用边界”很多人提到 React 就条件反射地想到“组件化”但真正理解组件化的人会知道它的价值不在“能把页面拆成几个文件”而在于复用边界切得足够清晰。所谓边界是指这个组件负责什么、不负责什么、通过哪些 props 和外部交流。我刚开始写 React 的时候也走过弯路喜欢把所有逻辑塞进一个巨型容器组件结果上线不到三个月就发现改不动了。后来接受了“组件就是函数”的思维输入 props输出 UI 和交互事件内部状态尽量私有。一个组件如果对外依赖超过五个 props通常意味着边界切歪了要么你把两个职责混在了一起要么你强行复用一个不该复用的部分。举个很典型的例子一个文章列表卡片组件要展示标题、摘要、封面图、阅读量。如果把“获取数据”的逻辑也写进卡片组件里那列表页和详情页各写一套数据获取卡片组件就只能服务这两个页面。反过来卡片组件只管接收 article 对象并渲染数据获取交给父级或请求层去处理这个组件就能在推荐流、收藏页、搜索页到处复用。组件化的本质是“接缝”的设计接缝对了后面一切重构都轻松。2. 吃透核心模型虚拟 DOM、单向数据流与函数式组件2.1 用“文档修改单”来理解虚拟 DOM虚拟 DOM 这个概念理论上谁都背得出来但真正理解它解决什么问题得换一个视角。你可以把真实 DOM 想象成一间贴满海报的房间每次改动都要物理地撕下来再贴新的成本很高。而虚拟 DOM 就是在内存里先拿一份已有的海报布局草稿把计划的新布局也画出来然后把两份草稿做一次 diff只挑出需要替换的那几张去操作。这就像一个编辑部在改文章时不直接撕稿子而是先在修订单上标记“第三段第二行改掉”最后统一对原稿执行少量操作。React 把读操作与写操作分离绝大多数状态更新先作用在虚拟描述上再经由调和过程转换为最小限度的真实 DOM 变更。这个设计让开发者在大多数场景下不必手工去 document.getElementById 然后逐个修改属性代码可维护性会大幅提升。但有一点要坦诚虚拟 DOM 并不是在所有场景都比手动操作真实 DOM 快。它的真正收益是“开发体验更好 在复杂状态变化下让性能保持在可接受区间”。如果你做一个只渲染一次然后永远不变化的页面虚拟 DOM 完全是多余的。理解了这一点你就不会拿着虚拟 DOM 去到处吹性能该用裸 DOM 的时候也能坦然接受。2.2 数据流到底单向在哪单向数据流的含义很简单数据从父组件流向子组件子组件只能用 props 读取不能直接改父组件的数据。如果子组件希望修改某个数据必须通过父组件传来的回调函数向上“申请”由数据所有者统一变更。我常把这种模式类比成公司里的报销流程。员工子组件自己不能把公司账上的钱直接划走他只能填报销单把单子递上去由财务父组件/状态容器审批后统一打款。这看起来多了一步但好处是任何时候你打开代码只要看状态定义的位置就能追到数据在哪些地方被触发过修改而不是像双向绑定那样整个项目到处都可能改了同一个字段排查时跟玩捉迷藏一样。在 React 中具体落到代码上就是父组件用 useState 或 reducer 持有数据同时把一个 onChange 回调传给子组件。子组件内部只做“收集输入并触发回调”这件事不持有真实业务数据的修改权。这种约束对大型项目尤其重要它让状态变化有迹可循也让多人协作时的代码冲突少很多。2.3 Hooks 演进背后是“关注点收敛”2018 年前写 React 类组件公共逻辑复用非常麻烦——要么高阶组件要么渲染插槽嵌套层级一多维护成本直线上升。Hooks 的引入之所以被称为颠覆性变革本质上是终于让“状态逻辑”被当成一等公民抽出来而不再被迫挂在组件实例的周期阶段上。useState 解决的是状态声明与更新的统一useEffect 解决的是把网络请求、事件订阅、DOM 操作这类副作用从生命周期里解放出来useMemo 和 useCallback 则解决渲染期计算和函数引用的稳定问题。更重要的是自定义 Hook 让多个组件之间可以共享同一套带状态的逻辑比如一个 useLocalStorageState 可以到处复用既想持久化又想在多组件间同步时不必复制粘贴。这里有一个容易被忽略的判断标准Hooks 并不是让你把代码写得更短而是让你把代码按“关注点”组织。写一个获取用户信息的组件class 时代你要把逻辑散落在 componentDidMount、componentDidUpdate、componentWillUnmount 里Hooks 时代一个 useEffect 就把取数、更新、取消订阅放在一段连续代码里。你能肉眼看到这段逻辑的完整路径而不是在生命周期里东一榔头西一棒子。3. 从脚手架到目录结构工程化落地要过的五道关3.1 脚手架选型与构建工具如果你跟 React 项目还是零基础起步现在最省心的路线是用 Vite 拉起一个 React TypeScript 模板。Vite 在开发服务器下的冷启动速度远好于 Webpack 时代热更新也顺滑日常写页面基本体验不到等待。相比 create-react-app 的一整套抽象配置Vite 的配置选项直观不少想要改 alias 或者加 proxy 时点进去就能找到。构建环境上Vite 底层用 esbuild 做依赖预构建用 Rollup 做生产打包。多数业务项目用它的默认配置就足够真正的优化点通常集中在“要不要拆 vendor”“代理怎么配”“资源怎么压缩”这几项上。我踩过一个比较实际的坑本地开发把代理目标写成了环境变量结果同事拉下来以后没配 .env.local所有接口 404。后来统一在项目根目录放了 .env.example并写明必须显式赋值代理目标才把这个问题压下去。3.2 目录结构按模块还是按技术层目录结构是个很容易吵起来的话题。按类型划分就是 components / hooks / pages / services 各放一层好处是定位简单坏处是一个功能的所有相关文件被拆进了五个地方修改一个需求要跨五个目录跳转。按模块划分就是每个业务域下放自己的组件、状态、请求文件例如 features/article 下面就是 article 的组件、接口、类型在一起好处是功能的聚合度高坏处是重复代码有时会潜伏得比较隐蔽。我目前更偏向“按模块为主按层为辅”的混合方式基础 UI 组件放 components公共 hooks 放 hooks但每个业务页面模块pages/xx内部再平铺强调 aggregation。这种方式迁移一个功能到别的项目时直接把模块目录拷走就行相当直观。至于彼此之间共享的类型和工具函数放 shared 层以避免模块间互相裸引。3.3 路由、请求与国际化初始化必做的三件事一个 React 项目的工程化骨架标配至少要解决三件事路由怎么配、请求怎么统一、多语言怎么安排。路由我建议用配置化模式而不是散写在 JSX 里把嵌套路由关系收敛到一个数组配合懒加载的 React.lazy 做按路由拆包。请求层每个项目都值得花一天时间封装一个统一实例把基础 URL、超时时间、统一错误处理、loading 控制都放进去而不是在每一个页面里都 new 一个 axios/fetch。国际化则是那种“项目一开始不上后面加就想把电脑砸了”的事。强烈建议在业务萌芽期就抽出一个 i18n 方案至少所有 UI 文案不要直接硬编码到 JSX 里统一走 t() 函数。到你真需要出海或做多语言移植时只需要补语言包。这三个基础做实了后面业务扩张会非常稳谁用谁知道。4. 状态管理从 useState 到 Zustand 的选型心路4.1 组件缓存、派生状态和可预测更新在聊状态管理库之前先把底层的状态分类理清。我把状态分成四类组件内部状态、跨组件共享状态、服务端缓存状态、URL 状态。组件内部状态用 useState/useReducer 就能解决不要再往外引额外的库。跨组件非频繁变化的状态比如主题色、语言、当前登录用户用 Context 足够。服务端缓存状态尽量用数据请求库去管让 loading、error、data 三态被统一封装。URL 状态则可以直接用路由参数承载比如分页页码、搜索关键词。这四类里最容易翻车的是混淆“页面临时状态”和“服务端数据”很多人会把接口数据直接塞进全局 store结果每个页面都得手动同步刷新。更好的做法是让请求层去处理数据来源组件只管消费。Store 里尽量只放真正的“客户端业务状态”比如购物车勾选清单、表单草稿、弹窗开关。4.2 Redux、MobX、Zustand 三种方案的取舍Redux 全家桶有完整的 devtools、中间件机制和社区积累适合那种状态流转非常复杂的场景比如大型协同编辑器、复杂表单动态配置。它的问题是样板代码确实多虽然 Redux Toolkit 已经剪掉大量模板但概念体系依然有学习成本。MobX 是另一种哲学用可观察数据和自动追踪响应写起来像是直接操作对象很直观。但它的“魔法感”较强隐式依赖会让团队里一些人感到不确定状态何时被追踪、何时不追踪调试时要靠理解响应式机制兜底。Zustand 则走轻量路线store 挂在组件树外部组件里用 hook 订阅可读没有 Provider 嵌套API 设计得很精简日常业务绝对够用。我个人的取舍逻辑很简单业务状态复杂度在“几个联动弹窗 两步表单”量级时直接 Zustand超过三个模块之间要实时同步同一份复杂数据再考虑 Redux Toolkit。这不是说 Redux 不好而是大部分业务的复杂度根本没有必要让样板代码来拖慢交付速度。4.3 我目前的默认组合现在的组合是所有接口请求交给统一的请求层 服务端状态工具处理客户端共享状态用 Zustand组件内部琐碎状态用 useState跨层级低频 props 用 Context。这套组合基本覆盖我遇到的绝大多数业务场景状态代码量少且每个状态的存活边界都很清楚。写 Zustand 时有几个小习惯值得说明store 定义成函数而不是对象字面量方便复用工厂逻辑异步操作也放进 store 的 action 里让组件只需要调用一个方法而不用关心请求逻辑selector 必须返回稳定的引用否则会引发无限重新渲染。最后一条尤其隐蔽我在某个项目里吃过亏后来统一养成了用浅比较或者直接选择原始值的习惯。5. 渲染性能优化从“能跑”到“流畅”5.1 先定位再优化哪些指标值得看性能优化最忌讳拍脑袋。我见过有人上来就把所有组件包一层 memo结果模块加载体积反而更大了收益根本察觉不到。正确流程是先记录问题是首次加载白屏很久还是交互时掉帧或者列表滚动不跟手然后针对性排查。首选工具是 React DevTools 的 Profiler 面板它能按帧和提交记录每个组件在本次更新里重渲染了没有、耗时多少非常直观。另一个是浏览器 Performance 面板看主线程里长任务分布判断是 JS 脚本执行、样式计算还是网络请求拖慢的。拿到数据再动手才是真正有效的优化思路。5.2 减少渲染次数memo、useMemo、useCallback 的正确用法React 组件的重渲染遵循“state 变则自身及子组件都重新走渲染函数”的规则。memo 是一个浅比较 props 的优化函数只有当 props 引用变化时才允许重渲。但 memo 真正起作用的前提是传入子组件的 props 本身没有“渲染期新值”。如果你在父组件渲染时直接写 children{() }或者传一个每次新建的数组memo 就形同虚设。useMemo 用来缓存计算型数据典型如过滤筛选后的列表、复杂统计结果。useCallback 则用来缓存函数引用让它不要因父组件重渲而换新从而配合 memo 保持子组件不更新。必须强调的是useCallback 只有落在“传给被 memo 包裹的子组件”这一路径里才明显有效单纯包一圈不加 memo不仅没用还白白增加开销。5.3 代码分割与虚拟列表首屏以外的体感优化面对大项目React.lazy 配合 Suspense 是切分首屏代码体积的基础操作。把路由级页面拆成独立 chunk用户访问对应路径时才加载对应 JS。除此之外体积敏感的部分还可以考虑 manualChunks 把第三方库单独拆包、开启 gzip 压缩传输。这些操作对指标的影响非常明确是最不依赖运行时技巧的提速方式。列表渲染是另一个高频瓶颈。几千条数据的表格直接渲染 DOM会让滚动变得非常吃紧。虚拟列表的思想是只渲染视口附近的内容窗口之外的项用占位高度代替。现在社区已有成熟的虚拟表格组件可用但我建议先确定列表是否真有这么多行、是否有固定列宽、是否需要动态行高再决定做不做虚拟化有些场景加一个 partition 分页栏就解决了根本不必引入虚拟列表的复杂度。6. 日常开发里最常踩的五个 React 坑6.1 key 用索引导致的“假更新”key 是 React 追踪列表元素的标识很多项目直接用了数组下标 index。这在“纯静态追加且不做排序”的场景里能跑但只要涉及列表中间插入、删除、排序问题就藏不住了。因为 React 靠 key 来区分新旧元素用 index 会把它和数组位置绑定一旦顺序变化组件内部状态就会跟着错位。我在一个可拖拽排序的看板组件里体会过这件事排序完成后卡片实际持有的本地状态属于排序前的编号界面看起来全乱。解决方式很简单数据实体只要有唯一 id 就一定用 id 做 key实在没有 id至少用“内容摘要 序号”这类稳定标识也不要裸用 index。6.2 setState 异步更新与闭包陷阱setState 的更新是异步合并的同一个事件循环里多次调用最终结果未必是你写代码时的直觉值。更隐蔽的是 useEffect 或事件回调里闭包捕获了旧的 state 值。比如你写了一个依赖 count 的副作用在事件处理器里调用 setCount但处理器里读到的 count 可能是上一次渲染时快照。解法有几个稳妥的方向想基于旧值更新就用函数式写法 setCount(c c 1)在异步无返回值的情况下想要最新值就考虑把 state 放进 ref 里同步维护一份或者在 reducer 中把更新逻辑集中描述。闭包陷阱没有银子弹还是要理解每一处读取是在“哪一次渲染的上下文”里。6.3 useEffect 依赖项的两种反模式第一种是盲目全部清空依赖只写空数组导致 effect 里去读取一个后续变化的 props 时拿到的是过期值通常表现为数据刷新不跟上交互。第二种是依赖数组里放了引用类型因为每次渲染数组都是新对象effect 再次触发可能形成无限循环。这两种模式本质上都是“没有诚实声明依赖”。正确姿势是effect 里读到了什么外部值就把它写进依赖哪怕某次觉得自己没用到。如果确实只期待挂载时执行同时又要读取最新 props就得用 ref 同步一份副本这样才能兼顾“只在挂载时订阅”和“读取时是新的”。6.4 StrictMode 下副作用执行两次React 在开发环境启用 StrictMode 后会用双调用的方式检查副作用是否可清理许多从来没见过这种行为的同学会以为是 bug。比如你在业务里写了一个 useEffect 定时器或者事件订阅开发模式下总会创建两次销毁一次结果看起来像是“请求发了两次”其实是 React 故意暴露你的清理函数没有把副作用完整回收。这个设计初衷是好的它逼你把清理逻辑写完整。如果你在 effect 里请求了接口清理函数里就应当老实地做一个 abort 或 ignore 标识如果注册了全局事件就及时 removeEventListener。真正上线时 StrictMode 的双调用不会发生但开发时看到重复执行不要急着关 StrictMode先检查自己是否少写了清理。6.5 自定义 Hook 命名与复用边界自定义 Hook 必须以 use 开头这不仅是约定更是保证 lint 能识别其中的 hooks 调用。如果你写了一个 getXXX 函数但里面又用了 useEffectlint 会直接警告同理自定义 Hook 不能放在条件语句里因为 hooks 的执行顺序必须是稳定的。复用边界方面我见过一个错误示范把一个“搜索商品”的完整业务逻辑全写进 useSearchProduct里面包含请求商品接口、多维度筛选、排序分页。表面看很酷但另一个项目只是要搜索用户这个 Hook 完全复用不了。合理的抽象粒度是把“分页状态”“请求状态”“筛选条件”分别拆小让它们可以自由组合而不是一次封装一个特大号逻辑。其实 React 这套生态走到今天最大的优势已经不是某个具体的 API 多好而是它逼迫你不断思考状态、渲染和副作用之间的关系。我前两年一度觉得这些概念很虚直到接手了一个臃肿的老项目才意识到凡是重构起来费劲的代码几乎都是状态边界模糊、副作用乱放造成的。后来在每个新需求里都坚持把这三件事分清楚代码质量和交付速度都上来了。如果你手里刚好有一个中大型交互项目要落地或者团队正准备从别的框架迁移过来我的建议是别一上来就堆满各种状态库和工具链先用最朴素的组件 props Hooks 写通一条业务路径。把核心模型吃透之后再去按真实痛点引入 Zustand、虚拟列表、代码分割这些手段。React 的上限不取决于某个库多强而取决于你对数据流的掌控力。
阅读完成 · 觉得有帮助?