近期准备面试,收录一些 React 面试题,扩充和复习知识面。加油加油(ง•_•)ง

React 面试题
6622 words
... views

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),在频繁更新的场景下会导致严重的性能问题。

工作流程:

  1. 初次渲染:React 根据组件树创建一棵完整的虚拟 DOM 树,然后映射为真实 DOM 挂载到页面上
  2. 状态变化:当组件的 state 或 props 发生变化时,React 会重新调用 render 方法生成一棵新的虚拟 DOM 树
  3. Diff 对比:React 通过 Diff 算法将新旧两棵虚拟 DOM 树进行逐层对比,找出差异部分
  4. 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.propsthis.state 访问的始终是最新值。这意味着在异步操作中(如 setTimeout),函数组件中读到的是触发时的值,类组件读到的是执行时的最新值。

React 官方推荐使用函数组件 + Hooks 的方式,因为它更简洁、更容易复用逻辑、没有 this 的心智负担。

React 组件的生命周期(类组件)h3

类组件的生命周期可以划分为三个阶段,每个阶段对应不同的生命周期方法:

挂载阶段(Mounting)——组件被创建并插入到 DOM 中:

  • constructor() → 初始化 state 和绑定事件方法。注意不要在 constructor 中调用 setState
  • static getDerivedStateFromProps(props, state) → 静态方法,根据新的 props 计算并返回新的 state。适用于 state 依赖于 props 的罕见场景
  • render() → 纯函数,返回 JSX。不应在此处执行副作用操作
  • componentDidMount() → DOM 挂载完成后调用,是发起网络请求、添加订阅、操作 DOM 的最佳时机

更新阶段(Updating)——当 props 或 state 发生变化时触发:

  • static getDerivedStateFromProps() → 同上
  • shouldComponentUpdate(nextProps, nextState) → 返回 boolean,决定是否需要重新渲染。PureComponent 通过浅比较自动实现此方法,是重要的性能优化入口
  • render() → 重新生成虚拟 DOM
  • getSnapshotBeforeUpdate(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 操作、订阅等)。它相当于类组件中 componentDidMountcomponentDidUpdatecomponentWillUnmount 的组合。

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。例如把 themesetTheme 分开,只需要 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
需要在深层组件中 dispatchuseReducer + Context

Redux vs Zustand vs Jotai 对比h3

特性Redux ToolkitZustandJotai
心智模型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

  1. 代码分割React.lazy + Suspense
const Dashboard = lazy(() => import('./Dashboard'))
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
  1. 列表虚拟化react-window / react-virtuoso
  2. 避免不必要渲染React.memo / useMemo / useCallback
  3. 状态下沉 — 状态就近管理,避免提升到顶层
  4. 防抖/节流 — 搜索输入等高频操作
  5. 图片懒加载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 的变化:

  • SwitchRoutes
  • component / renderelement
  • 相对路径嵌套路由
  • 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 的值。

// 读取 Promise
function 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 允许客户端直接调用服务端函数。

actions.js
'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 节点,这些节点通过 childsiblingreturn 三个指针形成链表结构(而非传统的树结构),支持高效的深度优先遍历和中断恢复:

{
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
普通普通更新网络请求回调
TransitionstartTransition 中的更新
空闲空闲任务预加载等

调度器使用 MessageChannel(而非 requestIdleCallback)来实现时间切片,每帧分配约 5ms 给 React 工作,剩余时间还给浏览器处理渲染和用户输入。


测试h2

React 组件测试策略h3

// 使用 @testing-library/react
import { 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()
})
})
// 测试自定义 Hook
import { renderHook, act } from '@testing-library/react'
test('useCounter', () => {
const { result } = renderHook(() => useCounter())
act(() => result.current.increment())
expect(result.current.count).toBe(1)
})

测试原则:

  • 测试行为,而非实现细节
  • 优先使用 getByRolegetByText 等语义化查询
  • 避免测试内部 state,关注用户可见的输出

评论