JSX 与基础概念h2
什么是JSX?为什么使用它?h3
JSX(JavaScript XML)是 React 引入的一种语法扩展,允许在 JavaScript 中编写类似 HTML 的标记。它本质上是 React.createElement() 的语法糖,使得开发者能够以声明式的方式描述 UI 结构。
// JSX 写法const element = <h1 className="title">Hello, world!</h1>
// 编译后等价于const element = React.createElement('h1', { className: 'title' }, 'Hello, world!')React.createElement() 返回的是一个普通的 JavaScript 对象(即 React 元素),描述了你期望在屏幕上看到的内容。React 读取这些对象,用它们来构建 DOM 并保持更新。
为什么使用 JSX:
- 直观性:JSX 让 UI 代码看起来像 HTML,直观地描述了页面结构,降低了心智负担
- 关注点聚合:将渲染逻辑和标记语言放在一起管理(组件内),而非将它们分离到不同文件中。这种方式使组件成为一个自包含的单元
- 类型安全:JSX 在编译阶段就能发现语法错误,而不是等到运行时
- 安全性:React DOM 在渲染之前会默认转义 JSX 中嵌入的任何值,因此可以有效防止注入攻击(XSS)。所有内容在渲染前都被转换成字符串
React 中的虚拟 DOM 是什么?工作原理?h3
虚拟 DOM(Virtual DOM)是真实 DOM 在内存中的轻量级 JavaScript 对象表示。每一个 React 元素就是一个虚拟 DOM 节点,它包含 type(标签名或组件)、props(属性)和 children(子节点)等信息。
之所以需要虚拟 DOM,是因为直接操作真实 DOM 的成本很高:每次 DOM 操作都可能触发浏览器的重排(Reflow)和重绘(Repaint),在频繁更新的场景下会导致严重的性能问题。
工作流程:
- 初次渲染:React 根据组件树创建一棵完整的虚拟 DOM 树,然后映射为真实 DOM 挂载到页面上
- 状态变化:当组件的 state 或 props 发生变化时,React 会重新调用 render 方法生成一棵新的虚拟 DOM 树
- Diff 对比:React 通过 Diff 算法将新旧两棵虚拟 DOM 树进行逐层对比,找出差异部分
- Patch 更新:将找到的差异以最小操作集的方式,批量应用到真实 DOM 上
Diff 算法的三个策略(复杂度从 O(n^3) 降到 O(n)):
- 树级别(Tree Diff):只比较同一层级的节点。如果一个节点在旧树中的父节点与新树不同,React 不会尝试复用它,而是直接销毁重建整棵子树。这意味着跨层级移动 DOM 节点的操作代价很大
- 组件级别(Component Diff):相同类型的组件会继续向下比较其子树;不同类型的组件(即使结构相似)也会被直接销毁并替换为新组件
- 元素级别(Element Diff):对于同一层级的一组子节点,React 通过
key属性来标识每个元素,判断它是新增、删除还是移动的
// key 的正确使用{ items.map((item) => ( <li key={item.id}>{item.name}</li> // Good: 使用稳定唯一的 id ))}
{ items.map((item, index) => ( <li key={index}>{item.name}</li> // Bad: 避免使用 index 作为 key ))}为什么不能用 index 作为 key?当列表发生插入、删除或排序时,index 和元素的对应关系会改变,React 会错误地复用组件实例,导致状态混乱和不必要的 DOM 操作。
React 中 key 的作用是什么?h3
key 是 React 用来追踪列表中每个元素身份的特殊属性。在 Diff 算法的元素级别对比中,key 起到了核心作用。
具体来说,key 有以下作用:
- 提高 Diff 效率:React 通过 key 来匹配新旧子元素。当 key 相同时,React 认为这是同一个元素,只需更新其属性;当 key 不同时,React 会销毁旧元素并创建新元素
- 保持组件状态:key 相同的组件会复用之前的实例和内部状态(如 useState 的值)。这意味着即使组件在列表中的位置发生了变化,只要 key 不变,它的状态也能正确保留
- 强制重新挂载:反过来,如果你想强制一个组件重置其内部状态,只需改变它的 key 值即可。React 会将旧组件卸载、新组件挂载,从而重新初始化所有状态
// 利用 key 重置组件状态// 当 userId 发生变化时,整个 UserProfile 组件会被卸载并重新挂载// 内部所有 state 都会重置为初始值,而不是残留上一个用户的数据<UserProfile key={userId} userId={userId} />选择 key 的原则:使用数据中稳定且唯一的标识符(如数据库 ID),避免使用数组索引或随机值。
React 事件机制与原生事件有何不同?h3
React 实现了一套自己的事件系统,称为「合成事件」(SyntheticEvent),它在原生 DOM 事件的基础上做了跨浏览器兼容处理,并提供了与原生事件相同的接口。
| 特性 | React 事件 | 原生事件 |
|---|---|---|
| 事件命名 | 驼峰式(onClick) | 全小写(onclick) |
| 事件处理 | 传入函数引用 | 传入字符串 |
| 阻止默认 | 必须 e.preventDefault() | 可 return false |
| 事件委托 | 统一委托到 root 节点 | 绑定在具体元素 |
| 事件对象 | SyntheticEvent(合成事件) | 原生 Event |
合成事件的工作原理:
React 并不会将事件处理函数直接绑定到对应的真实 DOM 节点上。React 17 之后,所有事件会被委托到应用的根 DOM 容器(root)上。当事件冒泡到根节点时,React 会根据事件的 target 找到对应的 Fiber 节点和事件处理函数,然后创建一个 SyntheticEvent 对象并执行回调。
合成事件的优势:
- 跨浏览器一致性:抹平了不同浏览器之间的事件差异,开发者无需关心兼容性问题
- 性能优化:通过事件委托机制,React 只在根节点注册少量事件监听器,而非为每个 DOM 元素都绑定事件,大幅减少了内存占用
- 统一管理:React 可以统一控制事件的优先级和批处理策略,配合并发模式实现更精细的更新调度
组件与生命周期h2
函数组件与类组件的区别?h3
在 React 中,组件可以用函数或 class 两种方式定义。两者最终都是返回 React 元素来描述 UI,但在实现方式和能力上有显著差异:
| 特性 | 函数组件 | 类组件 |
|---|---|---|
| 定义方式 | 普通函数 | class 继承 React.Component |
| 状态管理 | Hooks(useState) | this.state |
| 生命周期 | useEffect 模拟 | 完整的生命周期方法 |
| this 指向 | 无 this 问题 | 需要注意 this 绑定 |
| 性能 | 略优(无实例开销) | 有实例化开销 |
| 代码量 | 更简洁 | 相对冗余 |
深层区别:函数组件每次渲染都会捕获当前的 props 和 state(闭包特性),而类组件通过 this.props 和 this.state 访问的始终是最新值。这意味着在异步操作中(如 setTimeout),函数组件中读到的是触发时的值,类组件读到的是执行时的最新值。
React 官方推荐使用函数组件 + Hooks 的方式,因为它更简洁、更容易复用逻辑、没有 this 的心智负担。
React 组件的生命周期(类组件)h3
类组件的生命周期可以划分为三个阶段,每个阶段对应不同的生命周期方法:
挂载阶段(Mounting)——组件被创建并插入到 DOM 中:
constructor()→ 初始化 state 和绑定事件方法。注意不要在 constructor 中调用 setStatestatic getDerivedStateFromProps(props, state)→ 静态方法,根据新的 props 计算并返回新的 state。适用于 state 依赖于 props 的罕见场景render()→ 纯函数,返回 JSX。不应在此处执行副作用操作componentDidMount()→ DOM 挂载完成后调用,是发起网络请求、添加订阅、操作 DOM 的最佳时机
更新阶段(Updating)——当 props 或 state 发生变化时触发:
static getDerivedStateFromProps()→ 同上shouldComponentUpdate(nextProps, nextState)→ 返回 boolean,决定是否需要重新渲染。PureComponent 通过浅比较自动实现此方法,是重要的性能优化入口render()→ 重新生成虚拟 DOMgetSnapshotBeforeUpdate(prevProps, prevState)→ 在 DOM 更新前调用,返回值会传给 componentDidUpdate。常用于保存滚动位置等场景componentDidUpdate(prevProps, prevState, snapshot)→ DOM 更新完成后调用,可在此比较前后 props 来决定是否需要额外操作
卸载阶段(Unmounting)——组件从 DOM 中移除:
componentWillUnmount()→ 清理定时器、取消网络请求、移除事件监听等。不执行清理会导致内存泄漏
受控组件与非受控组件h3
这两个概念描述了 React 如何管理表单元素的数据。
受控组件:表单元素的值由 React 的 state 控制。每次用户输入都会触发 state 更新,然后 React 重新渲染组件,将新 state 值反映到输入框中。这意味着 React 成为了”唯一数据源”。
function ControlledInput() { const [value, setValue] = useState('') return <input value={value} onChange={(e) => setValue(e.target.value)} />}非受控组件:表单元素的值由 DOM 本身管理,React 不参与数据的更新过程。当需要读取值时,通过 ref 直接从 DOM 中获取。
function UncontrolledInput() { const inputRef = useRef(null) const handleSubmit = () => { console.log(inputRef.current.value) } return <input ref={inputRef} defaultValue="hello" />}如何选择:大多数场景推荐受控组件,因为它让 React 完全控制了数据流,便于表单验证、条件禁用按钮、强制输入格式等操作。非受控组件适用于文件上传(<input type="file">)或需要集成第三方非 React 库的场景。
高阶组件(HOC)是什么?h3
高阶组件(Higher-Order Component)是一个函数,它接收一个组件作为参数,返回一个新的增强组件。HOC 本身不是 React API,而是一种基于 React 组合特性的设计模式,用于复用组件逻辑。
function withLoading(WrappedComponent) { return function WithLoadingComponent({ isLoading, ...props }) { if (isLoading) return <Spinner /> return <WrappedComponent {...props} /> }}
const UserListWithLoading = withLoading(UserList)注意事项:
- 不要在 render 中使用 HOC(避免重复挂载)
- 需要复制静态方法(
hoist-non-react-statics) - Refs 不会自动传递,需
React.forwardRef
Hooks 深入h2
useState 的工作机制h3
useState 是最基础的 Hook,用于在函数组件中添加状态。调用后返回一个包含当前状态值和更新函数的数组。
const [state, setState] = useState(initialValue)底层机制:React 内部通过一个链表来记录每个组件中所有 Hook 的状态。每次组件渲染时,React 按照 Hook 的调用顺序依次读取链表中的值。这就是为什么 Hooks 不能放在条件语句或循环中——调用顺序必须在每次渲染中保持一致。
关键特性:
- 异步批量更新:调用 setState 不会立即更新状态,React 会将多次 setState 合并为一次重新渲染(React 18 自动批处理所有场景,包括 Promise、setTimeout 等)
- 函数式更新:当新状态依赖旧状态时,必须使用回调形式,否则可能读到过期的闭包值
// Bad: 可能拿到过期状态// 两次 setCount 都基于同一个闭包中的 count 值计算setCount(count + 1)setCount(count + 1) // 结果只 +1
// Good: 函数式更新,确保基于最新状态// prev 参数始终是上一次更新后的最新值setCount((prev) => prev + 1)setCount((prev) => prev + 1) // 结果 +2- 惰性初始化:当初始状态需要昂贵计算时,传入函数可以避免每次渲染都执行计算(函数只在组件首次渲染时调用一次)
// 只在组件挂载时计算一次,后续渲染不会再执行const [data, setData] = useState(() => expensiveComputation())- 状态不变性:setState 使用
Object.is比较新旧值,如果值相同则跳过重新渲染。因此更新对象或数组时必须创建新的引用
useEffect 完全指南h3
useEffect 让函数组件能够执行副作用操作(数据获取、DOM 操作、订阅等)。它相当于类组件中 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合。
useEffect(() => { // 副作用逻辑(挂载/更新时执行) return () => { // 清理函数(组件卸载时执行,或下次 effect 执行前执行) // 用于取消订阅、清除定时器、取消网络请求等 }}, [dependencies])依赖数组决定了 effect 的执行时机:
| 形式 | 执行时机 | 等价类组件生命周期 |
|---|---|---|
| 无依赖数组 | 每次渲染后执行 | 每次 componentDidUpdate |
[] 空数组 | 仅挂载时执行一次 | componentDidMount |
[a, b] | a 或 b 变化时执行 | componentDidUpdate 中比较 props |
常见陷阱 — 闭包过期问题:
function Counter() { const [count, setCount] = useState(0)
useEffect(() => { const timer = setInterval(() => { // 由于依赖数组为空,这个 effect 只在挂载时执行一次 // 内部的 count 永远是挂载时闭包捕获的值 0 console.log(count) // 始终打印 0 }, 1000) return () => clearInterval(timer) }, []) // 空依赖
// 解决方案:使用 ref 或添加依赖}useMemo 与 useCallback 的区别h3
这两个 Hook 都是性能优化工具,它们的核心作用是在组件重新渲染时跳过不必要的计算或对象创建。
| Hook | 缓存对象 | 用途 |
|---|---|---|
useMemo | 计算结果值 | 避免昂贵计算重复执行 |
useCallback | 函数引用 | 避免子组件因函数引用变化而重新渲染 |
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b])const memoizedFn = useCallback(() => doSomething(a), [a])
// useCallback(fn, deps) 等价于 useMemo(() => fn, deps)为什么需要 useCallback:函数组件每次渲染都会创建新的函数对象。如果将这个函数作为 props 传递给使用了 React.memo 的子组件,由于函数引用不同,子组件仍然会重新渲染。useCallback 通过缓存函数引用解决了这个问题。
不要滥用:useMemo 和 useCallback 本身也有开销(缓存存储、依赖比较),简单计算不需要 useMemo。只在性能分析确认瓶颈后才使用,或者在传递给 memo 子组件的回调上使用 useCallback。
useRef 的多种用途h3
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。这个对象在组件的整个生命周期中保持不变(同一个引用),且修改 .current 不会触发重新渲染。
// 1. 访问 DOM 元素(最常见用途)const inputRef = useRef(null)<input ref={inputRef} />inputRef.current.focus()
// 2. 保存可变值(不触发重新渲染)// 适合存储定时器 ID、WebSocket 实例等不需要渲染的数据const timerRef = useRef(null)timerRef.current = setInterval(...)
// 3. 跨渲染周期保存前一个值// ref 的更新发生在 useEffect 中(渲染后),所以读到的是上一次渲染的值function usePrevious(value) { const ref = useRef() useEffect(() => { ref.current = value }) return ref.current}自定义 Hook 的设计原则h3
自定义 Hook 是复用有状态逻辑的方式,以 use 开头。
function useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { try { const item = localStorage.getItem(key) return item ? JSON.parse(item) : initialValue } catch { return initialValue } })
const setValue = useCallback( (value) => { const valueToStore = value instanceof Function ? value(storedValue) : value setStoredValue(valueToStore) localStorage.setItem(key, JSON.stringify(valueToStore)) }, [key, storedValue] )
return [storedValue, setValue]}设计原则:
- 单一职责,每个 Hook 只做一件事
- 返回值语义清晰
- 处理好清理和边界情况
状态管理h2
React Context 的使用与性能问题h3
Context 提供了一种在组件树中跨层级传递数据的方式,无需通过 props 逐层传递(解决 prop drilling 问题)。
const ThemeContext = createContext('light')
function App() { const [theme, setTheme] = useState('light') return ( <ThemeContext.Provider value={{ theme, setTheme }}> <Page /> </ThemeContext.Provider> )}
function Button() { const { theme } = useContext(ThemeContext) return <button className={theme}>Click</button>}性能问题:当 Provider 的 value 发生变化时,所有调用 useContext 消费该 Context 的组件都会强制重新渲染,无论它实际使用的那部分数据是否改变。即使套了 React.memo 也无法阻止——Context 的更新会绕过 memo 的浅比较。
优化方案:
- 拆分 Context:将频繁变化的值和不常变化的值放入不同的 Context。例如把
theme和setTheme分开,只需要 dispatch 的组件不会因 theme 变化而重渲染 - useMemo 缓存 value:避免每次渲染创建新的 value 对象导致所有消费者更新
- 状态管理库替代:当 Context 引发的重渲染成为瓶颈时,考虑使用 Zustand、Jotai 等提供细粒度订阅的库
useReducer 与 useState 如何选择?h3
const [state, dispatch] = useReducer(reducer, initialState)
function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 } case 'decrement': return { count: state.count - 1 } default: throw new Error() }}| 场景 | 推荐 |
|---|---|
| 单一简单状态 | useState |
| 多个关联状态 | useReducer |
| 复杂状态转换逻辑 | useReducer |
| 需要在深层组件中 dispatch | useReducer + Context |
Redux vs Zustand vs Jotai 对比h3
| 特性 | Redux Toolkit | Zustand | Jotai |
|---|---|---|---|
| 心智模型 | Flux 单向数据流 | 简化版 Flux | 原子化状态 |
| 模板代码 | 中等 | 很少 | 极少 |
| 包大小 | ~12KB | ~1KB | ~3KB |
| DevTools | 完善 | 支持 | 支持 |
| 异步处理 | RTK Query/Thunk | 内置 | 内置 |
| 适用场景 | 大型应用 | 中小型应用 | 细粒度状态 |
性能优化h2
React.memo 的使用场景h3
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) { return items.map((item) => ( <div key={item.id} onClick={() => onSelect(item.id)}> {item.name} </div> ))})
// 自定义比较函数const MemoComp = React.memo(Component, (prevProps, nextProps) => { return prevProps.id === nextProps.id // true 跳过渲染})注意: 配合 useCallback 使用,否则每次父组件渲染创建新函数引用会使 memo 失效。
React 常见性能优化手段h3
- 代码分割 —
React.lazy+Suspense
const Dashboard = lazy(() => import('./Dashboard'))
<Suspense fallback={<Loading />}> <Dashboard /></Suspense>- 列表虚拟化 —
react-window/react-virtuoso - 避免不必要渲染 —
React.memo/useMemo/useCallback - 状态下沉 — 状态就近管理,避免提升到顶层
- 防抖/节流 — 搜索输入等高频操作
- 图片懒加载 —
loading="lazy"或 Intersection Observer
React 中如何避免不必要的重新渲染?h3
// 1. 状态拆分:将频繁变化的状态隔离function App() { return ( <> <FrequentUpdater /> {/* 频繁更新的部分独立成组件 */} <ExpensiveStatic /> {/* 不受影响 */} </> )}
// 2. children 模式:通过 children 传递避免重新渲染function Layout({ children }) { const [scroll, setScroll] = useState(0) return ( <div onScroll={(e) => setScroll(e.target.scrollTop)}> <Header scroll={scroll} /> {children} {/* children 不会因 scroll 变化而重渲染 */} </div> )}React Routerh2
React Router v6 核心概念h3
import { BrowserRouter, Routes, Route, Outlet, useParams, useNavigate } from 'react-router-dom'
function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Layout />}> <Route index element={<Home />} /> <Route path="users" element={<Users />}> <Route path=":id" element={<UserDetail />} /> </Route> <Route path="*" element={<NotFound />} /> </Route> </Routes> </BrowserRouter> )}
function Layout() { const navigate = useNavigate() return ( <div> <nav>...</nav> <Outlet /> {/* 嵌套路由出口 */} </div> )}v6 的变化:
Switch→Routescomponent/render→element- 相对路径嵌套路由
useNavigate替代useHistory
React 18 新特性h2
并发模式(Concurrent Mode)h3
React 18 引入了并发渲染,这是 React 架构层面最重大的变化。在并发模式下,渲染过程是可中断的——React 可以在渲染过程中暂停、恢复或放弃当前工作,优先处理更紧急的更新(如用户输入),从而保持界面的流畅响应。
与之前的同步渲染相比,同步模式下一旦开始渲染就必须完成,期间主线程被阻塞,用户的交互操作只能排队等待。并发模式解决了这个问题。
核心特性:
- 自动批处理:React 18 之前,只有事件处理函数中的状态更新才会自动批处理;在 Promise、setTimeout 等异步回调中的多次 setState 会分别触发渲染。React 18 开始,所有场景下的状态更新都会自动合并
// React 18 之前:setTimeout 中的更新不会批处理// React 18:自动批处理所有更新setTimeout(() => { setCount((c) => c + 1) setFlag((f) => !f) // 只触发一次重新渲染}, 1000)- Transitions:区分紧急和非紧急更新
import { useTransition, startTransition } from 'react'
function SearchPage() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [isPending, startTransition] = useTransition()
function handleChange(e) { setQuery(e.target.value) // 紧急:更新输入框 startTransition(() => { setResults(filterResults(e.target.value)) // 非紧急:可中断 }) }
return ( <> <input value={query} onChange={handleChange} /> {isPending ? <Spinner /> : <ResultList results={results} />} </> )}Suspense 与流式 SSRh3
// 客户端数据获取(配合支持 Suspense 的库);<Suspense fallback={<Skeleton />}> <Comments /></Suspense>
// 流式 SSR(React 18)import { renderToPipeableStream } from 'react-dom/server'
const { pipe } = renderToPipeableStream(<App />, { bootstrapScripts: ['/main.js'], onShellReady() { response.statusCode = 200 response.setHeader('content-type', 'text/html') pipe(response) },})useId Hookh3
为可访问性属性生成唯一 ID,在服务端和客户端保持一致。
function PasswordField() { const id = useId() return ( <> <label htmlFor={`${id}-password`}>密码</label> <input id={`${id}-password`} type="password" /> <p id={`${id}-hint`}>密码至少 8 位</p> </> )}React 19 新特性h2
React Compiler(自动记忆化)h3
React 19 引入编译器,自动对组件和 Hook 进行记忆化,减少手动 useMemo / useCallback / React.memo 的需要。
// React 19 之前需要手动优化const MemoComp = React.memo(({ data }) => { const processed = useMemo(() => expensiveWork(data), [data]) return <div>{processed}</div>})
// React 19 编译器自动处理,直接写即可function Comp({ data }) { const processed = expensiveWork(data) return <div>{processed}</div>}use() Hookh3
use() 是一个新的 API,可以在渲染时读取 Promise 或 Context 的值。
// 读取 Promisefunction Comments({ commentsPromise }) { const comments = use(commentsPromise) // Suspense 会处理 pending 状态 return comments.map((c) => <p key={c.id}>{c.text}</p>)}
// 读取 Context(可在条件语句中使用,useContext 不行)function Theme({ showTheme }) { if (showTheme) { const theme = use(ThemeContext) return <div className={theme} /> } return <div />}Server Components 与 Server Actionsh3
Server Components 在服务端运行,不增加客户端 bundle 大小。
// ServerComponent.jsx(默认是 Server Component)async function UserProfile({ userId }) { const user = await db.user.findById(userId) // 直接访问数据库 return <div>{user.name}</div>}
// ClientComponent.jsx;('use client')function LikeButton() { const [liked, setLiked] = useState(false) return <button onClick={() => setLiked(!liked)}>{liked ? 'Liked' : 'Like'}</button>}Server Actions 允许客户端直接调用服务端函数。
'use server'export async function updateUser(formData) { const name = formData.get('name') await db.user.update({ name })}
// Form.jsx;('use client')import { updateUser } from './actions'
function ProfileForm() { return ( <form action={updateUser}> <input name="name" /> <button type="submit">保存</button> </form> )}useFormStatus 与 useOptimistich3
// useFormStatus — 获取表单提交状态'use client'import { useFormStatus } from 'react-dom'
function SubmitButton() { const { pending } = useFormStatus() return <button disabled={pending}>{pending ? '提交中...' : '提交'}</button>}
// useOptimistic — 乐观更新function Messages({ messages, sendMessage }) { const [optimisticMessages, addOptimistic] = useOptimistic(messages, (state, newMsg) => [...state, { text: newMsg, sending: true }])
async function handleSend(formData) { const msg = formData.get('message') addOptimistic(msg) await sendMessage(msg) }
return ( <form action={handleSend}> {optimisticMessages.map((m) => ( <p key={m.text}>{m.text}</p> ))} <input name="message" /> </form> )}设计模式与最佳实践h2
React 常见设计模式h3
Render Props 模式:
function MouseTracker({ render }) { const [pos, setPos] = useState({ x: 0, y: 0 }) return <div onMouseMove={(e) => setPos({ x: e.clientX, y: e.clientY })}>{render(pos)}</div>}
;<MouseTracker render={({ x, y }) => ( <p> 位置:{x}, {y} </p> )}/>组合组件模式(Compound Components):
function Tabs({ children, defaultIndex = 0 }) { const [activeIndex, setActiveIndex] = useState(defaultIndex) return ( <TabsContext.Provider value={{ activeIndex, setActiveIndex }}> {children} </TabsContext.Provider> )}
Tabs.Tab = function Tab({ index, children }) { const { activeIndex, setActiveIndex } = useContext(TabsContext) return <button onClick={() => setActiveIndex(index)}>{children}</button>}
// 使用<Tabs> <Tabs.Tab index={0}>标签1</Tabs.Tab> <Tabs.Tab index={1}>标签2</Tabs.Tab></Tabs>错误边界(Error Boundary)h3
class ErrorBoundary extends React.Component { state = { hasError: false }
static getDerivedStateFromError(error) { return { hasError: true } }
componentDidCatch(error, errorInfo) { logErrorToService(error, errorInfo) }
render() { if (this.state.hasError) { return <h1>出了点问题</h1> } return this.props.children }}
// 使用;<ErrorBoundary> <UserProfile /></ErrorBoundary>注意:错误边界不能捕获事件处理函数、异步代码、SSR、自身的错误。这些场景请使用 try/catch。
React 项目结构最佳实践h3
src/├── components/ # 通用 UI 组件│ ├── Button/│ │ ├── Button.tsx│ │ ├── Button.test.tsx│ │ └── index.ts├── features/ # 按功能模块组织│ ├── auth/│ │ ├── components/│ │ ├── hooks/│ │ ├── services/│ │ └── types.ts├── hooks/ # 全局自定义 Hooks├── lib/ # 工具函数├── stores/ # 全局状态管理├── types/ # TypeScript 类型└── app/ # 路由与页面TypeScript 与 Reacth2
React 中 TypeScript 的常用类型h3
// 组件 Props 类型interface ButtonProps { variant: 'primary' | 'secondary' size?: 'sm' | 'md' | 'lg' children: React.ReactNode onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void}
// 事件处理const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { console.log(e.target.value)}
// Ref 类型const inputRef = useRef<HTMLInputElement>(null)
// 泛型组件interface ListProps<T> { items: T[] renderItem: (item: T) => React.ReactNode}
function List<T>({ items, renderItem }: ListProps<T>) { return <ul>{items.map(renderItem)}</ul>}Fiber 架构h2
React Fiber 是什么?解决了什么问题?h3
Fiber 是 React 16 引入的新协调引擎(Reconciler),核心目标是实现增量渲染——将渲染工作拆分为多个小单元(Unit of Work),可以暂停、恢复和按优先级排序。
解决的问题:
React 15 的 Stack Reconciler 采用递归方式同步遍历组件树,一旦开始就无法中断。当组件树很大时,会长时间阻塞主线程(可能超过 16ms),导致动画卡顿、用户输入无响应等体验问题。Fiber 将递归改为基于链表的可中断循环,使 React 能够在处理完一个 Fiber 节点后检查是否还有剩余时间,如果没有就让出主线程。
Fiber 节点的结构(简化):
每个 React 元素都对应一个 Fiber 节点,这些节点通过 child、sibling、return 三个指针形成链表结构(而非传统的树结构),支持高效的深度优先遍历和中断恢复:
{ type: ComponentType, // 组件类型(函数、类或标签名) key: string | null, stateNode: DOM | Instance, // 关联的 DOM 节点或组件实例 child: Fiber | null, // 第一个子节点 sibling: Fiber | null, // 下一个兄弟节点 return: Fiber | null, // 父节点(回溯指针) pendingProps: object, // 本次渲染待处理的 props memoizedState: object, // 上次渲染完成后的 state(Hooks 链表也存在这里) lanes: number, // 优先级通道(用二进制位表示)}双缓冲机制(Double Buffering): React 始终维护两棵 Fiber 树:current 树代表当前屏幕上显示的内容,workInProgress 树是正在内存中构建的新版本。当 workInProgress 树构建完成后,React 通过将根节点的 current 指针指向 workInProgress 树来完成切换(一次指针赋值),避免了中间状态闪烁。旧的 current 树则成为下次更新的 workInProgress 树(复用 Fiber 节点,减少内存分配)。
React 的调度机制(Scheduler)h3
React 的调度器基于优先级来安排工作:
| 优先级 | 类型 | 示例 |
|---|---|---|
| 最高(同步) | 离散事件 | click、input |
| 高 | 连续事件 | scroll、mousemove |
| 普通 | 普通更新 | 网络请求回调 |
| 低 | Transition | startTransition 中的更新 |
| 空闲 | 空闲任务 | 预加载等 |
调度器使用 MessageChannel(而非 requestIdleCallback)来实现时间切片,每帧分配约 5ms 给 React 工作,剩余时间还给浏览器处理渲染和用户输入。
测试h2
React 组件测试策略h3
// 使用 @testing-library/reactimport { render, screen, fireEvent, waitFor } from '@testing-library/react'
test('搜索功能', async () => { render(<SearchComponent />)
const input = screen.getByPlaceholderText('搜索...') fireEvent.change(input, { target: { value: 'React' } })
await waitFor(() => { expect(screen.getByText('React 教程')).toBeInTheDocument() })})
// 测试自定义 Hookimport { renderHook, act } from '@testing-library/react'
test('useCounter', () => { const { result } = renderHook(() => useCounter())
act(() => result.current.increment())
expect(result.current.count).toBe(1)})测试原则:
- 测试行为,而非实现细节
- 优先使用
getByRole、getByText等语义化查询 - 避免测试内部 state,关注用户可见的输出
评论