ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

React核心语法实战:从JSX原理到Hooks状态管理与性能优化

React核心语法实战:从JSX原理到Hooks状态管理与性能优化 1. JSX不是HTML先弄清楚React的渲染本质React的核心语法说来说去都绕不开JSX。很多人刚接触React时很容易把它当作一种写在JavaScript里的HTML结果一写就踩坑——标签属性名写错、样式对象写错、注释写法不对、条件渲染渲染出了false或0。这些问题本质上都因为没理解JSX到底是什么。JSX不是模板引擎也不是HTML超集。它的真实身份是语法糖Babel会把JSX代码编译成React.createElement(type, props, children)调用。比如你写div classNamecard h1标题/h1 /div编译结果大致是React.createElement( div, { className: card }, React.createElement(h1, null, 标题) )React.createElement最终返回的是一个普通的JavaScript对象这个对象有type、props、children等字段。React运行时拿到这个对象树去对比之前的对象树也就是diff然后才真正操作DOM。换句话说JSX本质上在描述组件结构的数据而不是直接写标签。理解了这一点很多问题就好解释了。1.1 为什么外层必须有且只有一个根节点既然React.createElement是嵌套结构返回的必然是一棵树那自然只能有一个根。React 16之前想返回多个平级元素必须包一层divReact 16.2之后提供了Fragment语法 li第一项/li li第二项/li /这里要注意空标签并不是特殊组件它一样会被编译但不会在DOM里产生任何节点。所以以后如果你的页面莫名其妙多了一堆空div检查一下是不是没用Fragment。1.2 属性名的差异不是React故意折腾你JSX属性名和原生HTML属性有几个显著差异class必须写成className因为class在JavaScript里是保留字for必须写成htmlFor原生label标签会踩这个坑tabindex要写成tabIndexreadonly要写成readOnly自定义属性必须用>const style { backgroundColor: #fff, fontSize: 14px, marginTop: 12 };CSS属性名一律用小驼峰。数字类型的值React会默认给你加px某些属性除外比如lineHeight、flex、opacity等。这一套规则第一次用确实容易记混但用熟了之后会发现比写字符串拼接舒服很多。另一个常见误解是注释。JSX里的注释写法比较反直觉div {/* 这是注释 */} {/* 多行注释 这行也是注释 */} /div因为花括号里是表达式上下文注释必须用/* */包裹。新手经常写// 注释然后报错就是因为没理解花括号里是JS表达式这一层。2. 组件与props单向数据流的设计哲学组件是React的第二个核心概念。函数组件和Class组件在React 16.8之前差异较大如今hooks普及后函数组件已经是绝对主流。但不管是哪种组件的本质都是一个输入props、输出JSX的函数function Greeting({ name, age }) { return p你好{name}年龄{age}/p; }这个函数每次被调用都对应一次渲染的过程。React不知道也不关心你怎么实现的它只关心你返回的JSX长什么样。2.1 props是只读的为什么不能改props是父组件传给子组件的数据。在React的心智模型里数据是单向流动的从父组件流向子组件子组件只能读不能改。如果子组件改了props里的对象属性虽然JS层面不报错但会造成不可预测的UI状态——因为父组件根本不知道这个变化下一次重新渲染时数据又被覆盖回去了。实践中如果子组件确实需要修改某个值有两条路把修改的方法作为props传给子组件子组件调用父组件传下来的回调由父组件去改状态子组件把传入的数据复制到自己的useState或useRef里再管理第一条是React官方推荐的做法它保证了唯一数据源在父组件手里UI永远和状态保持一致。这也是为什么React的单向数据流能带来清晰的可预测性——任何时刻UI就是当前props和state的一个快照。2.2 props.children组合能力的基础props.children是React组合模型的核心。你写Card p我是卡片内容/p /Card这个p标签就会作为Card组件的children传入。这不是什么魔法它就是一个普通对象属性。利用这个机制可以做flexible的布局组件function Layout({ header, sidebar, children }) { return ( div div{header}/div aside{sidebar}/aside main{children}/main /div ); }调用时这样传Layout header{Header /} sidebar{aside导航/aside} Content / /Layout你会发现组件之间搭积木的能力靠的就是props.children。这也是React前端生态里高阶组件、render props等模式的底层来源。3. useState与useEffecthooks生态下的状态管理React 16.8引入了hooks这是React核心语法的重要转折。useState和useEffect是日常开发中使用频率最高的两个hooks但也是最容易出问题的两个。3.1 useState的反模式很多人刚用useState时会有这些疑惑为什么state更新了但界面没变为什么setState后立刻读值还是旧值useState返回的set函数触发更新后会触发一次重新渲染但当前这一次函数运行环境中变量值仍然是旧值。因为每次渲染都是重新执行一次组件函数新状态只属于下一次渲染function Counter() { const [count, setCount] useState(0); const handleClick () { setCount(count 1); console.log(count); // 仍然是0 }; return button onClick{handleClick}点击/button; }这不是bug这是React的渲染模型决定的。如果你要基于最新state做后续操作可以用函数式更新或者在useEffect里监听变化setCount(prev prev 1)。另一个常见反模式是把对象直接塞进state然后修改属性const [user, setUser] useState({ name: , age: 0 }); // 错误 user.name 张三; setUser(user); // React会认为state没变UI不更新必须返回一个新的对象引用setUser({ ...user, name: 张三 });React比较state变没用值比较或者深比较而是用的Object.is级别的浅比较。只要你没有产生新引用React就认为没有变化。记住永远不要直接修改state永远用set函数生成新值。3.2 useEffect的心智模型很多新手把useEffect理解为生命周期钩子其实这个说法有误导。useEffect更接近在渲染提交到屏幕之后执行副作用的机制。依赖数组是重点中的重点useEffect(() { // 每次渲染都执行 }); useEffect(() { // 只有在挂载时执行一次严格模式下会执行两次 }, []); useEffect(() { // 当count变化时执行 }, [count]);依赖数组不是参数组成的数组而是这个effect依赖哪些值的声明。如果你在effect内部引用了某个变量却没把它写进依赖数组React会给你warning但不会帮你修复——这个bug往往很隐蔽。举个例子一个常见的轮询需求useEffect(() { const timer setInterval(() { fetchData(id); }, 1000); return () clearInterval(timer); }, [id]);id变化后会重新创建定时器同时也清理了旧的定时器。这里如果不把id放进去fetchData(id)拿到的永远是初次渲染的那个id这就是闭包过期的经典问题。在异步请求、事件监听、定时器这些场景里特别容易出现。另一个值得注意的点是effect的清理函数。在使用事件监听、订阅、定时器时一定要在清理函数里解除绑定否则组件卸载后还会继续执行副作用轻则内存泄漏重则报错。React 18之后开发环境下effect会在Mount后执行一次并立即Cleanup再重新执行这不是性能问题这是为了帮你发现漏清理的Bug。4. 事件处理与表单受控组件与非受控组件的取舍React事件体系有个显著特点事件是合成事件也就是说React在顶层统一挂了代理监听而不是把事件直接绑定在DOM元素上。这主要是为了跨浏览器一致性也顺便贡献了自动清理和性能层面的优势。事件处理函数里需要注意this指向Class组件时代这是个重点坑函数组件时代已经不存在了但事件对象的池化机制仍然是常见话题。React 17之前事件对象是会被复用的异步访问事件对象属性会得到空值需要调用event.persist()。React 17之后池化机制被移除了现在可以放心在异步里使用event但为了兼容旧项目还是建议先确认一下版本。表单是React中的一个高频难点核心在于受控组件和非受控组件的选择。4.1 受控组件数据驱动UI的正统方案所谓受控就是表单input的值由React state控制function Form() { const [value, setValue] useState(); const handleChange (e) { setValue(e.target.value); }; return input value{value} onChange{handleChange} /; }用户输入 - onChange事件 - setState - 重新渲染 - 输入框值变为state值这就是受控的完整闭环。好处是input的值不会再自行变化所有数据流都有迹可循可以随时对用户输入做过滤、格式化、限制长度等。比如移动端项目里常见的语音输入功能把语音识别结果直接setValue(transcript)输入框的值立刻同步更新这就是受控组件的天然优势。你想剪贴板粘贴一组数据、想用微信扫码回填内容、想批量替换文本所有从外部注入输入框的场景受控组件都是最简单的解法。4.2 非受控组件适合哪些场景非受控组件用ref直接访问DOMfunction Uncontrolled() { const inputRef useRef(null); const submit () { console.log(inputRef.current.value); }; return input typetext ref{inputRef} /; }没有value和onChange的绑定值由DOM自己管理。这类组件适合表单提交时才取值、不关心内容的输入场景、第三方组件封装、文件上传等不需要实时校验的字段。一个不够成熟的建议是能用受控尽量用受控非受控适合少数明确不需要React掺和内部状态的场景。但反过来也别把所有表单都硬写成受控——比如一个用户要输入一长串文章内容的textarea每次键盘敲击都触发一次state更新和重渲染在大文本场景下确实有性能压力。合理做法是让子表单组件独立收货父组件提交时再统一取值。5. 条件渲染与列表渲染最容易出错的两个高频写法条件渲染和列表渲染是React JSX里最日常的写法却也是新手栽跟头最多的地方。5.1 逻辑与运算符的坑0和会被渲染出来很多教程会写{condition Component /}这种模式简洁高效。但一旦condition不是布尔值而是数字或字符串就会翻车// 假设 count 0 {count p当前有{count}条消息/p} // 页面上会渲染出 0原因在于0 something在JavaScript里返回的是0React渲染这种数字时会直接把它显示出来。而false something返回falseReact什么都不渲染。所以安全写法是{count 0 p当前有{count}条消息/p}或者先转成布尔值{!!count ...}。这个坑在从后端拿数量数据时特别容易踩——接口返回0页面莫名其妙显示了一个0。5.2 key的作用别看它小写错会出灵异bug列表渲染必须加key这个应该已是共识。但key怎么选很多人还是懵。key的真实作用是帮助React识别哪个元素发生了什么变化——是新增、删除还是移动。React的diff算法会尽量复用如果key选错可能发生明明只是插入了一条数据结果其他项的状态全丢了、输入框内容串了、滚动位置乱了。key选择的三条经验优先用数据里唯一的ID比如业务ID、创建时间戳等没有ID时可以用index但必须保证列表是静态的、不发生排序和中间增删绝对不要用随机数当key这会让React每次渲染都认为所有项都变了性能急剧下降一个典型反例是分页列表用户在第一页输入表格内容翻到第二页再翻回来之前输入的内容全没了。很可能就是因为用了index当keyReact不知道这条数据还是原来那条直接重渲染了。更深层的理解是key属于同层兄弟节点间的比较规则。不同父节点下的key即使相同也没关系key只在同一个数组里需要唯一。6. 样式方案内联、CSS Modules与CSS-in-JS的取舍样式这块React官方没给标准答案社区方案五花八门。核心语法层面能说的主要有三类。6.1 内联样式适合动态样式和简单变量内联样式本质是对象好处是可以直接绑定JS变量function ProgressBar({ percent }) { return ( div style{{ width: ${percent}%, background: percent 80 ? red : green }} 进度 /div ); }但内联样式不支持伪类、媒体查询、动画也不支持样式复用。组件稍微复杂一点就不好维护。所以它的定位是局部动态样式、组件内联主题变量、小型工具组件。6.2 CSS Modules和CSS-in-JSCSS Modules是工程化方案通过构建工具把类名编译成带hash的唯一类名天然不用操心类名冲突// Button.module.css 里 .btn 会被编译为 .Button_btn__3xY7a import styles from ./Button.module.css; function Button() { return button className{styles.btn}按钮/button; }好处是零学习成本、静态CSS的性能优势、易于抽公共变量。坏处是动态样式穿插起来麻烦一点和JS变量沟通需要通过行内样式或CSS变量。CSS-in-JSstyled-components等则是用JS模板字符串写CSSconst Button styled.button background: ${props props.primary ? blue : gray}; padding: 8px 16px; ;它把样式和组件绑在一个文件里动态样式顺手想做主题切换也方便。代价是运行时开销——每次渲染都要解析样式字符串或者注入style标签对性能敏感的大型列表、图表页面会有压力。我自己的经验是组件库项目适合CSS Modules或者直接纯CSS业务页面小中型项目用CSS-in-JS体验很爽动态样式多的场景可以用内联配合CSS变量。核心思路不是选一个最强的而是别跨方案乱混。一个项目里三种方案都上那才是真正的灾难。7. 状态管理进阶useReducer、Context与全局状态的边界当state不再只是一个组件自己用而是多个兄弟组件、跨层级组件都要读写时就需要考虑状态管理方案了。7.1 useReducer逻辑复杂的局部状态useReducer是useState的升级版适合状态变化逻辑多的场景function reducer(state, action) { switch (action.type) { case increment: return { count: state.count 1 }; case decrement: return { count: state.count - 1 }; case reset: return { count: 0 }; default: throw new Error(); } } function Counter() { const [state, dispatch] useReducer(reducer, { count: 0 }); return ( button onClick{() dispatch({ type: increment })} {state.count} /button ); }它比useState强的地方在于把状态怎么变的逻辑从组件里抽出去变成纯函数。action是描述意图的对象reducer根据action返回新状态。规律是如果你的一个组件里有三四个state变量互相影响、一个操作要改多个字段那就该换useReducer了。7.2 Context的合理使用范围React Context用于跨越层级传递数据避免prop drilling。但它不是性能化的全局状态工具——Context值变化时所有消费该Context的组件都会重新渲染即使它们只关心某一部分数据。如果有人用Context管理一整个大型全局state那性能往往撑不住。比较合理的用法是把若干个Context按领域拆小比如UserContext、ThemeContext、SettingsContext并且消费端组件尽量拆分细粒度。const ThemeContext React.createContext(light); function App() { const [theme, setTheme] useState(light); return ( ThemeContext.Provider value{{ theme, setTheme }} Layout / /ThemeContext.Provider ); } function Toolbar() { const { theme } useContext(ThemeContext); return div style{{ color: theme dark ? #fff : #000 }}工具栏/div; }你不需要一上来就上Redux或Zustand。很多中小项目里useReducer 若干Context足够用。只有真正出现跨页面跨模块的复杂共享状态、需要devtools时间旅行调试、需要持久化中间件时再引入外部状态库。8. 性能优化memo、useCallback和useMemo的实战判断React渲染本身很快但遇到大列表、图表、复杂表单时仍然需要注意性能。React默认行为是父组件重新渲染子组件也跟着渲染——这不是bug这是React保证UI一致性的最简单方式。但很多场景里子组件并没有必要跟着重新跑一遍render。8.1 React.memo阻止不必要的子组件重新渲染React.memo对组件做浅比较props不变时就跳过重新渲染const MemoChild React.memo(function Child({ data }) { return div{data.name}/div; });等等有一个关键前提如果父组件每次渲染时都创建一个新数组或对象作为props那浅比较永远会失败function Parent() { const data { name: 张三 }; // 每次渲染都是新对象 return MemoChild data{data} /; }要让memo生效就得配合useMemofunction Parent() { const data useMemo(() ({ name: 张三 }), []); return MemoChild data{data} /; }所以React.memo和useMemo是两兄弟很难分开用。8.2 useMemo和useCallback的正确姿势useMemo缓存计算结果useCallback缓存函数引用const total useMemo(() list.reduce((acc, i) acc i.price, 0), [list]); const handleClick useCallback(() { console.log(total); }, [total]);滥用也无必要useMemo本身有hash比较的开销缓存一个简单运算可能比直接计算还慢。合理的场景是——list变化频率低、计算本身昂贵、结果被多个子组件使用、或者结果作为依赖数组的值。经验门槛如果运行它的代价小于运行多个组件重新渲染的代价就值得用否则别用。另外补充一个细节useCallback和useMemo的依赖数组写好后基本不会再从外部修改但如果内部调用了ref或外部状态依赖数组漏写会造成闭包过期这和useEffect一个道理。8.3 大列表和图表场景的优化思路热词里提到了React图表这种场景特别考验性能。渲染数千条数据时不加优化每帧都可能卡顿。常用手段React.memo包裹每个图表项useMemo缓存格式化后的数据不在渲染中重复计算虚拟列表实际渲染可视窗口内条目的react-window等方案图表库尽量选SVG或Canvas渲染可控的数据量特别大时直接换WebGL方案还有一个容易被忽略的点减少state放在大列表项内部。每个列表项自身的state变化只有自己会重新渲染这没问题但如果列表项中的某个指标值是从全局state读的那全局state一变化所有项都要重渲染。合理做法是让全局state只保留最小必要集合派生数据用useMemo在组件内部计算。9. React 18与未来语法并发渲染、SSR数据预获取React 18把并发渲染Concurrent Mode推向了稳定版本这给核心语法带来了几个不可回避的变化。9.1 自动批处理React 18之前setState在事件处理里是自动批处理的但在Promise、setTimeout、原生事件里不会会导致多次渲染。React 18开始所有场景都会自动批处理function handleClick() { setCount(c c 1); setFlag(f !f); // 只重新渲染一次 }这带来的心智变化是记住状态更新可能是异步、延迟的不要在setState之后立刻依赖DOM或state。必要时可以用flushSync强制同步刷新但日常场景很少需要。9.2 useTransition与useDeferredValue并发特性引入的两个新hooks值得了解。useTransition用于标记低优先级更新让高优先级的交互不被阻塞function App() { const [isPending, startTransition] useTransition(); const [list, setList] useState([]); const handleSearch (keyword) { // 输入框能保持流畅搜索结果的更新可以延迟 startTransition(() { setList(filterData(keyword)); }); }; }useDeferredValue则是让某个值延迟更新常用于输入框联动列表搜索场景。这两个API解决的都是交互优先级问题核心语法层面了解一下即可实际项目中可以显著优化体验。9.3 React SSR数据预获取的新方向热词里提到react ssr 数据预获取方案React 18的Suspense正在改变服务端渲染的数据获取方式。传统SSR要等所有数据就绪才返回HTMLSuspense方案则允许组件挂起直到数据准备完成可以把首屏时间分散到最需要的部分。用框架层面做这类事情比如Next.js的RSC、Remix的loader会更方便核心语法层面只需要知道Suspense可以指定fallback让等待异步数据成为声明式而非命令式。Suspense fallback{Spinner /} AsyncProfile userId{userId} / /Suspense这是React并发能力在数据获取领域的重要延伸也是面试里React新特性的高频问题方向。10. 写在最后怎么真正掌握React核心语法最后分享一点我自己的体会。React核心语法不难难的是心智模型转变。很多人写了好几个月React仍然在用React写jQuery——总想直接操作DOM、总想从一个全局变量读数据、总对重新渲染感到手足无措。我的建议是不要急着上脚手架先用Create React App或Vite搭一个最小的项目把本篇文章里的每个示例都亲手敲一遍。特别是useEffect依赖数组、useReducer的action设计、Context的拆分这些只靠看永远学不会踩一次坑比看十遍文档都有用。面试方面React核心语法的考点其实也很集中JSX编译原理、key的作用、受控与非受控、hooks依赖数组的闭包陷阱、memo和useMemo的适用边界、React 18批处理与并发特性。把这些问题每一个都能从为什么会这样讲清楚而不是背答案基本就没问题了。学React的过程中你会发现它逼着你用更结构的眼光看待视图层的复杂性和状态变化。前面讲的所有语法点本质上都是围绕UI是状态的函数这个核心思想展开的。抓住这条主线后面再看Redux、React Router、Next.js这些生态你会豁然开朗。
返回列表