React面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 455月28日 03:20

React 项目中常见的内存泄漏场景有哪些?

核心场景React 组件卸载后,与之关联的副作用如果没有同步清除,就会产生内存泄漏。实际项目中主要分四类:异步操作更新已卸载组件的状态在组件内发起 fetch 请求,组件卸载了请求才回来,此时 setState 会触发 React 的 warning(React 18 下已移除该 warning,但逻辑上的泄漏仍然存在)。setTimeout/setInterval 同理——组件卸载了定时器还在跑,回调里访问了过期的 state 或调用 setState。// 泄漏写法useEffect(() => { fetch('/api/data').then(res => res.json()).then(setData);}, []);// 修复:用 AbortController 取消请求useEffect(() => { const controller = new AbortController(); fetch('/api/data', { signal: controller.signal }) .then(res => res.json()) .then(setData) .catch(err => { if (err.name !== 'AbortError') throw err; }); return () => controller.abort();}, []);事件监听未移除在 useEffect 里给 window、document 或 DOM 节点添加了 resize、scroll、keydown 等监听,但 cleanup 里没有 removeEventListener。监听回调持有组件作用域的引用,组件实例无法被 GC 回收。useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize);}, []);闭包持有大对象引用useCallback/useMemo/useRef 的闭包里引用了不再需要的大对象(比如接口返回的完整列表、Blob 数据等),只要闭包还在,这些对象就无法被回收。常见于列表页缓存整个数据集却只展示部分。// 泄漏:闭包引用了全量数据const filteredList = useMemo(() => largeList.filter(fn), [largeList]);// 优化:只保留需要的字段,尽早释放原引用const filteredList = useMemo( () => largeList.map(item => ({ id: item.id, name: item.name })).filter(fn), [largeList]);未清理的订阅与第三方实例WebSocket、EventSource、IntersectionObserver、ResizeObserver、MutationObserver 这些 API 都需要手动 close()/disconnect()。第三方库(如 Redux 的 subscribe、RxJS 的 Observable.subscribe、ECharts 实例)同理,组件卸载时必须调用对应的销毁方法。useEffect(() => { const ws = new WebSocket('wss://example.com'); ws.onmessage = (e) => setMessage(JSON.parse(e.data)); return () => ws.close();}, []);修复思路核心原则:useEffect 的 setup 和 cleanup 必须对称——setup 里获取了什么资源,cleanup 里就要释放什么。具体做法:异步请求用 AbortController 取消定时器用 clearTimeout/clearInterval 清除DOM 事件用 removeEventListener 移除订阅类 API 调用 close()/disconnect()/unsubscribe()大对象引用用 useRef 配合手动置 null 释放追问useEffect 的 cleanup 什么时候执行?组件卸载时,或者下一次 effect 执行前(依赖项变化时先跑旧 cleanup 再跑新 effect)。React 18 StrictMode 下开发模式会额外执行一次 setup+cleanup 来暴露遗漏的清理逻辑。怎么排查 React 内存泄漏?Chrome DevTools → Memory 面板 → 取 Heap Snapshot。操作组件(挂载→卸载→挂载→卸载),对比两次 snapshot。如果 Detached DOM 节点或组件实例数量持续上升,说明有泄漏。也可以用 React DevTools Profiler 观察 React 组件树是否出现已卸载组件仍存在的现象。React 18 对内存泄漏有什么影响?React 18 移除了 "Can't perform a React state update on an unmounted component" 的 warning,但泄漏本身并未消失——未清理的闭包引用和事件监听仍然存在。同时 StrictMode 在开发模式下双重挂载组件,更容易暴露 cleanup 遗漏的问题。此外并发特性(Suspense、useTransition)引入了组件挂载-卸载-重新挂载的新生命周期,对 cleanup 的正确性要求更高。
前端阅读 895月28日 03:16

什么是 Virtual DOM?React 为什么用它替代直接操作 DOM?

Virtual DOM 是一棵用 JavaScript 对象描述的 DOM 树。React 状态变化时,先在内存里生成新的虚拟 DOM 树,和旧树做 diff,算出最小变更集,再批量更新真实 DOM。这么做的核心原因是——直接操作真实 DOM 太贵了,一次 appendChild 可能触发重排+重绘+合成三层渲染管线。Virtual DOM 把"手动精确定位变更"这件事自动化了:你只管声明 UI 长什么样,React 负责高效地同步到真实 DOM。顺带一提,正因为 UI 描述和渲染层解耦,React Native 才能复用同一套组件模型渲染原生控件。追问Virtual DOM 一定比直接操作 DOM 快吗?不是。单改一个文本节点,el.textContent = 'xxx' 比 Virtual DOM diff 整棵子树更快。Virtual DOM 的价值在复杂 UI 场景:几十个组件同时更新时,它能自动算出最优更新路径,避免开发者手动 diff。所以它不是"最快",而是"在绝大多数场景下足够快,且不需要你操心"。React 的 diff 算法怎么做到 O(n) 的?朴素的树 diff 是 O(n³),React 用三个策略降到 O(n):| 策略 | 含义 ||------|------|| 类型不同直接重建 | <div> 变成 <span>,整个子树丢弃重建,不跨类型 diff || key 标识稳定性 | 通过 key 追踪同一组子元素中的身份,避免错位复用 || 只比较同级 | 不跨层级比较,父亲和儿子不会互相匹配 |代价是可能遗漏极少数跨层级移动的最优解,但实际场景中跨层级移动极少,这个权衡是值得的。Vue 的 Virtual DOM 和 React 有什么区别?Vue 的响应式系统精确追踪了"哪个组件依赖了哪个数据",数据变了可以直接跳过无关组件的 diff。React 默认从触发更新的组件开始整棵子树 diff,需要 React.memo、useMemo 手动优化。但 React 18 并发模式可以在调度层面拆分任务、让高优先级更新插队,这是 Vue 目前没有的。实际项目里 Virtual DOM 有什么常见的坑?index 做 key:列表增删元素时 diff 错位,导致非预期复用和状态错乱。用唯一 ID 做 key。大列表没有虚拟化:Virtual DOM 只解决 diff 效率,不解决渲染量。几千行的长列表必须上 react-window / react-virtuoso 做虚拟滚动。不必要的重渲染:父组件 state 变了,即使子组件 props 没变也会 diff。React.memo 和 useMemo 不是过早优化,是 React 开发的日常操作。
服务端阅读 05月28日 02:51

useCallback 和 useMemo 有什么区别?什么场景下使用?

面试官问:"useCallback 和 useMemo 有什么区别?什么场景下使用?"——这道题几乎出现在每一场 React 岗位的面试中。答案其实不复杂,但很多人答完区别就卡在使用场景上,要么说"都用上总没错",要么完全不知道什么时候该用。核心区别:一句话记住useCallback 缓存函数引用,useMemo 缓存计算结果。// 这两行等价const fn = useCallback(() => doSomething(a), [a]);const fn = useMemo(() => () => doSomething(a), [a]);useCallback(fn, deps) 本质上就是 useMemo(() => fn, deps) 的语法糖。记住这个等式,很多困惑会自动消散。| 特征 | useCallback | useMemo ||------|-------------|---------|| 返回值 | 函数本身 | 函数的执行结果 || 缓存对象 | 函数引用 | 任意值(对象、数组、基本类型) || 典型场景 | 传给子组件的回调 | 昂贵计算 / 保持引用稳定 || 一句话 | "别重新创建这个函数" | "别重新算这个值" |为什么需要它们:React 重渲染机制React 函数组件每次渲染都会重新执行整个函数体。你在组件里写的每一行代码——定义变量、创建函数、计算表达式——每次渲染都重新跑一遍:function MyComponent({ items }) { const handleClick = () => console.log("clicked"); // 每次渲染都创建新函数 const filtered = items.filter(item => item.active); // 每次渲染都重新过滤 return <Child onClick={handleClick} data={filtered} />;}如果 items 有 10000 条,而组件每秒渲染 60 次,你就在每秒过滤 60 万次。更麻烦的是,handleClick 每次都是新的引用——如果 Child 用了 React.memo,它期待的"不变引用"就白费了。useCallback 和 useMemo 的作用就是告诉 React:"如果依赖没变,把上次的结果还给我。"useCallback:保持函数引用稳定配合 React.memo 阻止子组件无效渲染这是 useCallback 最核心的使用场景:function Parent({ items }) { // ❌ 每次渲染都创建新函数,Child 的 React.memo 白费了 const handleClick = () => { console.log("clicked"); }; // ✅ 函数引用稳定,Child 不会因它而重渲染 const handleClick = useCallback(() => { console.log("clicked"); }, []); return <Child onClick={handleClick} items={items} />;}const Child = React.memo(({ onClick, items }) => { console.log("Child render"); return <button onClick={onClick}>Click</button>;});关键点:useCallback 单独用效果有限,必须配合 React.memo 才能阻止子组件重渲染。如果子组件没有 React.memo 包裹,父组件渲染它就会渲染——useCallback 改变不了这一点。作为 useEffect 的稳定依赖function UserProfile({ userId }) { // ❌ fetchUser 每次都是新引用,useEffect 每次都会执行 const fetchUser = () => { api.getUser(userId); }; // ✅ 只在 userId 变化时重新创建 const fetchUser = useCallback(() => { api.getUser(userId); }, [userId]); useEffect(() => { fetchUser(); }, [fetchUser]);}不过这里有个陷阱:用 useCallback 缓存函数再放进 useEffect 依赖,实际上你是间接依赖了 callback 的真实依赖。对于上面的例子,直接在 useEffect 里写请求逻辑、直接依赖 userId 更直接。这个模式主要用在自定义 Hook 中,函数需要暴露给外部使用:function useUserData(userId) { const fetchUser = useCallback(() => { return api.getUser(userId); }, [userId]); return { fetchUser }; // 调用方拿到的引用是稳定的}自定义 Hook 中的 useCallback这是面试中容易忽略的场景。当你写一个自定义 Hook 返回方法时,如果不加 useCallback,消费方每次拿到的都是新函数,它的 useEffect 会被反复触发:function useTable(pagination) { const refresh = useCallback(() => { fetchData(pagination); }, [pagination]); const reset = useCallback(() => { setFilters({}); setPagination({ page: 1 }); }, []); return { refresh, reset }; // 消费方可以安全地放入依赖数组}useMemo:缓存计算结果避免重复的数组操作function ProductList({ products, filter }) { // ❌ 每次渲染都重新过滤 const filteredProducts = products.filter(p => p.name.includes(filter)); // ✅ 只在 products 或 filter 变化时重新计算 const filteredProducts = useMemo(() => products.filter(p => p.name.includes(filter)), [products, filter] ); return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>;}复杂计算(排序、聚合、数据转换)function DataTable({ data }) { const sortedData = useMemo(() => { return [...data].sort((a, b) => a.score - b.score); }, [data]); const chartData = useMemo(() => { return data.reduce((acc, item) => { const key = item.category; if (!acc[key]) acc[key] = { total: 0, count: 0 }; acc[key].total += item.value; acc[key].count += 1; return acc; }, {}); }, [data]); return <Chart data={chartData} />;}保持对象和数组的引用稳定这个用法很容易被忽略,但在性能优化中很重要:function Parent({ items }) { // ❌ 每次渲染创建新对象,子组件 React.memo 失效 const style = { color: "red" }; const config = { threshold: 0.5, rootMargin: "0px" }; // ✅ 对象引用稳定 const style = useMemo(() => ({ color: "red" }), []); const config = useMemo(() => ({ threshold: 0.5, rootMargin: "0px" }), []); return <Child style={style} observerConfig={config} />;}useCallback 缓存函数,useMemo 缓存值——这是一个重要的互补关系。当你需要传一个稳定引用的对象或数组给子组件时,用 useMemo。useMemo 和 useRef 的区别面试中经常追加这个问题。两者都能"记住"上一次的值,但机制完全不同:| 特征 | useMemo | useRef ||------|---------|--------|| 触发重渲染 | 依赖变化时返回新值,组件正常重渲染 | 修改 .current 不触发重渲染 || 用途 | 缓存计算结果 | 持久化可变值(DOM 引用、前一次渲染的值等) || 依赖追踪 | 有依赖数组,自动更新 | 无依赖,手动管理 |// useMemo:依赖变了才重算,结果参与渲染const sorted = useMemo(() => data.sort(), [data]);// useRef:记住上一次的值,但不触发重渲染const prevCount = useRef(count);useEffect(() => { prevCount.current = count;}, [count]);如果你只是想跨渲染记住一个值但不影响渲染输出,用 useRef;如果你需要基于依赖缓存计算结果参与渲染,用 useMemo。组合使用:性能优化三层架构在一个复杂的列表组件中,三个 Hook 经常协同工作:function SearchResults({ query, data }) { // 第一层:缓存数据计算结果 const results = useMemo(() => { return data.filter(item => item.name.toLowerCase().includes(query.toLowerCase()) ); }, [data, query]); // 第二层:缓存事件处理函数 const handleItemClick = useCallback((id) => { console.log("Selected:", id); }, []); // 第三层:缓存传递给子组件的 props 对象 const listProps = useMemo(() => ({ items: results, onItemClick: handleItemClick }), [results, handleItemClick]); return <ResultList {...listProps} />;}这种"数据 → 函数 → props 对象"的三层缓存,是 React 性能优化的标准范式。但记住,这是在已经发现性能问题之后的优化手段,不是写代码时的默认操作。用 DevTools 验证优化效果说了这么多"什么时候该用",那怎么判断你写的 useCallback / useMemo 真的有用?靠猜是不行的,用 React DevTools Profiler:打开 Chrome DevTools → Profiler 标签点击录制按钮,操作你的组件停止录制,查看火焰图找到不必要的重渲染(灰色条表示"没变但重渲染了")针对性地加 useCallback / useMemo,再录一次对比如果你加了缓存但 Profiler 没有变化,说明这个缓存是多余的——移除它。优化从来不是越多越好。三个最常见的坑坑一:过度使用——简单运算不需要缓存// ❌ 两个数相加也要 useMemo?缓存的成本比计算还大const total = useMemo(() => a + b, [a, b]);// ❌ 简单的字符串拼接也要 useCallback?const label = useCallback(() => `${firstName} ${lastName}`, [firstName, lastName]);// ✅ 直接写const total = a + b;const label = `${firstName} ${lastName}`;经验法则:计算操作耗时 < 1ms 的,不需要 useMemo。只有循环遍历大数组、递归、复杂对象转换才值得。useCallback 同理——如果子组件没被 React.memo 包裹,你加不加 useCallback 效果一样。坑二:闭包陷阱——遗漏依赖// ❌ multiplier 在闭包里但不在依赖数组const multiplier = 2;const result = useMemo(() => value * multiplier, [value]);// ✅ 所有引用的外部变量都要声明const result = useMemo(() => value * multiplier, [value, multiplier]);务必开启 eslint-plugin-react-hooks 的 exhaustive-deps 规则,让 ESLint 帮你检查。闭包陷阱不是"偶尔遇到"的问题,是"迟早遇到"的问题。坑三:useCallback 配合了没 memo 的子组件// ❌ 子组件没有 React.memo,useCallback 基本白用function Parent() { const handleClick = useCallback(() => {}, []); return <PlainChild onClick={handleClick} />;}没有 React.memo 的子组件,父组件渲染它就会渲染——useCallback 改变不了这一点。这是一个非常常见的"写了等于没写"的场景。React 19 Compiler 会取代它们吗?React 19 引入了 React Compiler(实验性),可以自动为代码插入等效的 useMemo 和 useCallback。如果你的项目已经启用了 Compiler,手动写这些 Hook 的需求会大幅减少。但目前绝大多数项目(React 16-18)仍然需要手动优化。而且即使有了 Compiler,理解 useCallback 和 useMemo 的原理,能帮你在遇到性能问题时快速定位根因——Compiler 不是万能的,它也会犯错,这时候你需要知道底层的运作方式来判断是 Compiler 的 bug 还是你代码的问题。面试速答模板面试中被问到这道题,建议这样组织答案:区别:useCallback 缓存函数引用,useMemo 缓存计算结果。useCallback(fn, deps) 等价于 useMemo(() => fn, deps),是它的语法糖。场景:useCallback 主要配合 React.memo 阻止子组件无效渲染,或作为自定义 Hook 的稳定返回值;useMemo 用于昂贵计算和保持对象/数组引用稳定。注意点:两者都不是"越多越好"。没有 React.memo 的子组件加 useCallback 无效;简单计算加 useMemo 反而更慢;务必开启 exhaustive-deps 规则避免闭包陷阱。追问准备:React 19 Compiler 可以自动处理大部分缓存需求,但理解原理对定位问题仍然必要。useRef 也能"记住"值但不触发重渲染,和 useMemo 的触发机制不同。总结| 你需要 | 用什么 | 关键搭档 ||--------|--------|----------|| 缓存函数给子组件 | useCallback | React.memo || 缓存计算结果 | useMemo | — || 缓存对象/数组引用 | useMemo | React.memo || 防止 useEffect 不必要触发 | useCallback / useMemo | 依赖数组 |记住三点:先写对,再优化。等 React DevTools Profiler 告诉你哪里慢了再动手,不要预先给所有东西加缓存useCallback 是 useMemo 的特例,缓存的是函数引用,不是计算结果没有 React.memo 的子组件,useCallback 基本是自我安慰
服务端阅读 05月28日 02:37

如何创建和使用 Zustand store?

核心答案Zustand 通过 create 函数创建 store,返回一个可直接在组件中使用的 Hook。与 Redux 不同,它不需要 Provider 包裹,store 本身就是 Hook:import { create } from 'zustand'const useStore = create((set, get) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), reset: () => set({ count: 0 }),}))组件中使用时,推荐通过选择器订阅,避免不必要的重渲染:const count = useStore((state) => state.count) // 只订阅 countconst increment = useStore((state) => state.increment) // 只订阅 incrementset 与 get 的用法set 用于更新状态,支持对象和函数两种形式。Zustand 自动浅合并第一层属性,所以不需要手动展开 ...state:const useStore = create((set) => ({ user: { name: 'Tom', age: 20 }, // 对象形式:直接替换第一层属性 setName: (name) => set({ user: { name, age: 20 } }), // 注意:第二层需手动处理 // 函数形式:基于旧状态计算 incrementAge: () => set((state) => ({ user: { ...state.user, age: state.user.age + 1 } })),}))get 用于在 action 中读取当前状态,不触发订阅:const useStore = create((set, get) => ({ items: [], addItem: (item) => set({ items: [...get().items, item] }), getCount: () => get().items.length, // 不触发重渲染}))选择性订阅与性能优化直接解构整个 store 会导致任何状态变化都触发重渲染,应避免:// 不推荐:任何状态变化都触发重渲染const { count, name } = useStore()// 推荐:按需订阅const count = useStore((s) => s.count)const name = useStore((s) => s.name)对于复杂对象,使用 shallow 比较避免引用变化导致的重渲染:import { shallow } from 'zustand/shallow'const { name, age } = useStore( (s) => ({ name: s.user.name, age: s.user.age }), shallow)Store 拆分(Slice 模式)大型应用中,将不同领域的状态拆成独立 slice,再合并到一个 store:// slices/cartSlice.jsexport const createCartSlice = (set) => ({ items: [], addItem: (item) => set((s) => ({ items: [...s.items, item] })), clearCart: () => set({ items: [] }),})// slices/userSlice.jsexport const createUserSlice = (set) => ({ user: null, setUser: (user) => set({ user }),})// store.jsimport { create } from 'zustand'import { createCartSlice } from './slices/cartSlice'import { createUserSlice } from './slices/userSlice'const useStore = create((...a) => ({ ...createCartSlice(...a), ...createUserSlice(...a),}))异步操作Zustand 的 action 可以直接是 async 函数,不需要额外的中间件:const useStore = create((set) => ({ data: null, loading: false, error: null, fetchData: async (id) => { set({ loading: true, error: null }) try { const res = await fetch(`/api/data/${id}`) const data = await res.json() set({ data, loading: false }) } catch (error) { set({ error: error.message, loading: false }) } },}))常用中间件persist — 持久化到 localStorageimport { create } from 'zustand'import { persist } from 'zustand/middleware'const useStore = create( persist( (set) => ({ theme: 'light', setTheme: (theme) => set({ theme }), }), { name: 'theme-storage' } // localStorage key ))immer — 不可变更新的简化写法import { create } from 'zustand'import { immer } from 'zustand/middleware/immer'const useStore = create( immer((set) => ({ user: { name: 'Tom', address: { city: 'Beijing' } }, setCity: (city) => set((state) => { state.user.address.city = city }), // 无需手动展开,直接修改 draft })))devtools — Redux DevTools 调试支持import { create } from 'zustand'import { devtools } from 'zustand/middleware'const useStore = create( devtools((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })), }), { name: 'CounterStore' }))中间件可以组合使用,顺序从外到内:devtools(persist(immer(...)))。create 与 createStore 的区别| | create | createStore ||---|---|---|| 返回值 | React Hook | Store 对象 || 使用场景 | React 组件内 | React 外(测试、服务端、非React环境) || 订阅方式 | useStore(s => s.xxx) | store.subscribe() / store.getState() |import { createStore } from 'zustand'const store = createStore((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })),}))// React 外部使用store.getState().count // 读取store.setState({ count: 10 }) // 更新store.subscribe((state) => { // 监听 console.log('state changed', state)})追问:Zustand 与 Redux 的核心区别是什么?无需 Provider:Zustand 不需要 <Provider> 包裹组件树,直接导入 Hook 使用订阅粒度:Zustand 通过选择器精确订阅,Redux 用 useSelector 实现类似效果但机制不同样板代码:Zustand 无 action type、reducer、dispatch,一个函数搞定Bundle 体积:Zustand ~1KB vs Redux Toolkit ~11KB中间件生态:Redux 有更成熟的中间件链,Zustand 的中间件更轻量但够用
服务端阅读 05月28日 02:10

什么是 React Query,它解决了前端开发中的哪些常见问题?

React Query(现名 TanStack Query)是一个专门管理服务器状态的异步状态管理库。它解决的核心问题是:把原本散落在 useEffect + useState + 全局状态库中的数据获取逻辑,收敛到声明式的 Hook 调用中。它解决的关键问题1. 手动管理请求状态的样板代码没有 React Query 时,每个请求都要写 loading、error、data 三件套:// 传统写法const [data, setData] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);useEffect(() => { fetch("/api/todos") .then(res => res.json()) .then(setData) .catch(setError) .finally(() => setLoading(false));}, []);用 React Query 一行替代:const { data, isLoading, isError } = useQuery({ queryKey: ["todos"], queryFn: () => fetch("/api/todos").then(res => res.json()),});2. 缓存与重复请求React Query 基于 queryKey 自动缓存响应数据。相同 key 的多个组件只发一次请求,后续直接读缓存。通过 staleTime 和 gcTime 控制数据新鲜度和缓存回收。3. 数据同步与过期刷新内置 stale-while-revalidate 策略:先展示缓存数据,后台静默刷新。窗口重新聚焦、网络恢复时自动重新请求,无需手动监听事件。4. 竞态条件当快速切换页面或筛选条件时,早期请求的响应可能覆盖最新数据。React Query 自动取消过期请求,确保结果与当前 queryKey 匹配。5. 服务端状态与客户端状态混用Redux 管客户端 UI 状态没问题,但把服务端数据也塞进去会导致:手动写 loading 状态、手动处理缓存失效、手动同步更新。React Query 把服务端状态单独抽出来,让 Redux 只管 UI 状态。追问staleTime 和 gcTime 的区别是什么?staleTime 控制数据何时标记为过期(触发后台刷新),gcTime 控制缓存多久后被垃圾回收(彻底删除)。useMutation 如何配合 queryClient.invalidateQueries 实现乐观更新?先通过 onMutate 回调更新缓存,失败时在 onError 中回滚。React Query v5 相比 v4 有哪些破坏性变更?移除了 onError/onSuccess 全局回调,统一用 queryClient.getQueryData 替代。为什么说 React Query 是异步状态管理器而非数据请求库?它不关心请求怎么发(fetch/axios/GraphQL 都行),只管 Promise 的状态流转和缓存策略。
服务端阅读 05月28日 01:18

Zustand 与 Redux 相比有哪些优缺点?

Zustand 的核心优势极简 API,告别样板代码Zustand 创建 store 只需一个函数调用,无需定义 action types、reducers、action creators。与 Redux Toolkit 的 createSlice 相比,代码量减少 60% 以上:// Zustand — 一个函数搞定 storeimport { create } from 'zustand'const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })),}))// 组件中直接用 hookfunction Counter() { const { count, increment } = useStore() return <button onClick={increment}>{count}</button>}// Redux Toolkit — 需要更多概念import { createSlice, configureStore } from '@reduxjs/toolkit'import { Provider, useSelector, useDispatch } from 'react-redux'const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1 }, decrement: (state) => { state.value -= 1 }, },})const store = configureStore({ reducer: { counter: counterSlice.reducer } })// 还需要 Provider 包裹 + useSelector + useDispatchfunction Counter() { const count = useSelector((s) => s.counter.value) const dispatch = useDispatch() return <button onClick={() => dispatch(increment())}>{count}</button>}无需 Provider,更干净的组件树Redux 必须在应用顶层包裹 <Provider store={store}>,导致组件树多出一层嵌套,测试时也需要用 <Provider> 包裹。Zustand 直接在模块作用域创建 store,组件通过 hook 消费状态,无需任何包裹组件。这对存量项目集成尤其友好——不用改已有的组件树结构就能接入状态管理。更小的体积,更快的加载| 库 | Gzipped 大小 ||---|---|| Zustand | ~1.1 KB || Redux Toolkit | ~8.2 KB || Redux Toolkit + React-Redux | ~11.8 KB |在 3G 网络下,11 KB 的差距可带来 50-100ms 的交互时间优化。对包体积有严格限制的移动端 H5 场景,Zustand 优势明显。内置选择器,精准控制重渲染Zustand 支持细粒度订阅,只订阅需要的状态切片,自动跳过无关更新:// 只订阅 count,其他状态变化不会触发重渲染const count = useStore((state) => state.count)// 也支持 shallow 比较对象import { shallow } from 'zustand/shallow'const { count, name } = useStore( (state) => ({ count: state.count, name: state.name }), shallow)Redux 的 useSelector 同样支持选择器,但默认使用 === 严格比较,返回对象时需要手动使用 shallowEqual,容易遗忘导致不必要的重渲染。TypeScript 开箱即用Zustand 的 store 定义自带类型推导,无需额外声明类型:const useStore = create<{ count: number; increment: () => void }>((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })),}))// count 和 increment 类型自动推导,无需泛型Redux Toolkit 虽然也有良好的 TS 支持,但 createSlice 和 configureStore 需要更多类型标注和泛型配置。异步操作更直观Zustand 处理异步不需要额外中间件,直接写 async 函数:const useStore = create((set) => ({ data: null, loading: false, fetchData: async () => { set({ loading: true }) const res = await fetch('/api/data') const data = await res.json() set({ data, loading: false }) },}))Redux Toolkit 通过 createAsyncThunk 处理异步,需要定义 pending/fulfilled/rejected 三种状态,样板代码更多。Zustand 的不足生态和中间件相对薄弱Redux 拥有 Redux Saga、Redux Observable、Redux Persist 等成熟中间件生态,处理竞态、取消、重试等复杂副作用有成熟方案。Zustand 自带 persist、devtools、immer 等中间件,覆盖常见需求,但社区中间件数量和成熟度仍远不及 Redux。遇到复杂副作用场景时,往往需要自己实现或组合多个中间件。调试体验有差距Zustand 可通过 devtools 中间件接入 Redux DevTools,但时间旅行调试和 action 回放功能不如原生 Redux 完善。对于需要严格追踪每次状态变更、回溯 bug 的场景,Redux 的调试体验更可靠。大型项目实践尚在积累Zustand 已被 React Three Fiber、shadcn/ui、Next.js 示例等项目广泛采用,2026 年 npm 周下载量达 700 万次,但在超大型企业级应用中的最佳实践仍不如 Redux 成熟。缺少千人团队的治理模式参考和大规模重构案例。灵活性的双刃剑Zustand 不强制状态更新模式,不同开发者可能写出风格迥异的 store。在缺乏 Code Review 约束的团队中,灵活反而变成混乱。Redux 的严格单向数据流天然约束了风格统一性,新人接手代码时理解成本更低。什么时候选 Zustand中小型项目:状态逻辑不复杂,追求开发速度和简洁性存量项目集成:不想引入 Provider 改动组件树,零侵入接入性能敏感场景:对包体积和重渲染有严格要求,如移动端 H5快速原型开发:重视迭代速度而非架构完备性React Three Fiber / 可视化项目:Zustand 是 R3F 生态的默认选择什么时候选 Redux大型企业级应用:团队 5 人以上,需要标准化流程和严格约束复杂副作用:需要 Saga 级别的竞态处理、取消和重试能力重度调试需求:时间旅行调试对排查线上问题至关重要团队已熟悉 Redux:迁移成本高于收益,不必为了换而换金融 / 交易类系统:需要追踪每一次状态变更的审计场景怎么选:一句结论项目小、求快、求轻选 Zustand;项目大、求稳、求规范选 Redux。两者并非互斥——复杂项目中可以在全局状态用 Redux、局部状态用 Zustand,按需搭配。
服务端阅读 05月28日 01:18

Zustand 中的 set 函数有几种使用方式?

Zustand 的 set 函数有三种使用方式,面试中需要完整回答。1. 对象式更新——直接传入新状态const useStore = create((set) => ({ count: 0, reset: () => set({ count: 0 })}));适用于更新不依赖当前状态值的场景,写法简洁。set 会将传入的对象与当前状态浅合并,未提及的字段保持不变。2. 函数式更新——基于当前状态计算新值const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 }))}));函数接收当前 state,返回要合并的对象。异步操作中必须用这种写法,否则闭包会捕获过期的 state:// 错误:count 来自闭包,可能是旧值const badIncrement = async () => { await delay(1000); set({ count: count + 1 });};// 正确:state 始终是最新的const goodIncrement = async () => { await delay(1000); set((state) => ({ count: state.count + 1 }));};3. 替换模式——完全替换而非合并set 的第二个参数 replace 默认为 false(浅合并)。设为 true 时,传入的对象会完全替换当前状态,而非合并:const useStore = create((set) => ({ count: 0, name: 'demo', // 只剩 count,name 被清除 resetAll: () => set({ count: 0 }, true)}));实际开发中常用于登出时清空整个 store,或切换用户时重置所有字段。三种方式的对比| 方式 | 签名 | 状态处理 | 适用场景 ||------|------|----------|----------|| 对象式 | set(partial) | 浅合并 | 不依赖旧值的更新 || 函数式 | set((state) => partial) | 浅合并 | 依赖旧值、异步操作 || 替换式 | set(partial, true) | 完全替换 | 重置 store |浅合并的行为细节浅合并只做第一层合并,嵌套对象需要手动展开:const useStore = create((set) => ({ user: { name: 'Tom', age: 20 }, // 错误:age 丢失 badUpdate: () => set({ user: { name: 'Jerry' } }), // 正确:展开后再覆盖 goodUpdate: () => set((state) => ({ user: { ...state.user, name: 'Jerry' } }))}));如果嵌套层级深,可以配合 immer 中间件来简化写法。面试追问:set 为什么默认浅合并而不是深合并?性能。深合并需要递归遍历整个状态树,对大多数场景来说是不必要的开销。浅合并只需一次 Object.assign 级别的操作,配合 React 的引用比较就能高效判断是否需要重渲染。需要深合并时,开发者自行选择 immer 等工具即可。
服务端阅读 05月28日 00:38

React Query 中如何实现乐观更新?它有哪些优缺点?

乐观更新(Optimistic Update)是 React Query 的核心特性之一,它让应用在服务器响应返回之前就更新 UI,用户操作能获得即时反馈,体验更接近原生应用。乐观更新的工作原理乐观更新的核心思路是"先斩后奏":用户触发操作时,立刻把预期结果写入缓存更新 UI,同时发起真实请求;如果服务器确认成功,用真实数据替换乐观数据;如果失败,则回滚到操作前的状态。整个生命周期分为四步:onMutate — 取消进行中的查询,保存当前缓存快照,写入乐观数据请求发出 — mutation 函数执行,等待服务器响应onError(失败时)— 用快照回滚缓存,恢复 UIonSettled(无论成败)— 让相关查询失效,拉取服务器最新数据基础实现以更新待办事项为例,完整的乐观更新代码如下:const mutation = useMutation({ mutationFn: updateTodo, onMutate: async (updatedTodo) => { // 1. 取消进行中的查询,防止竞态覆盖 await queryClient.cancelQueries({ queryKey: ['todos'] }); // 2. 保存当前缓存,用于回滚 const previousTodos = queryClient.getQueryData(['todos']); // 3. 乐观写入缓存 queryClient.setQueryData(['todos'], (old: Todo[]) => old.map(todo => todo.id === updatedTodo.id ? { ...todo, ...updatedTodo } : todo ) ); // 4. 返回上下文,onError 中可拿到 return { previousTodos }; }, onError: (_err, _variables, context) => { // 失败时回滚 if (context?.previousTodos) { queryClient.setQueryData(['todos'], context.previousTodos); } }, onSettled: () => { // 最终和服务器同步 queryClient.invalidateQueries({ queryKey: ['todos'] }); },});// 触发mutation.mutate({ id: 1, title: '更新后的标题' });为什么需要 cancelQueries?这是面试中经常被追问的点。如果不取消正在进行的查询,可能出现这种情况:onMutate 刚把乐观数据写入缓存,但一个正在后台执行的 refetch 随后返回,把乐观数据覆盖掉。cancelQueries 会中止这些进行中的请求,确保乐观更新不会被意外冲掉。新增数据的乐观更新上面的例子是更新已有数据,比较简单。新增数据时有一个额外问题:新项没有服务端返回的真实 ID,需要生成临时 ID,成功后再替换。onMutate: async (newTodo) => { await queryClient.cancelQueries({ queryKey: ['todos'] }); const previousTodos = queryClient.getQueryData(['todos']); // 生成临时 ID const tempId = `temp-${Date.now()}`; queryClient.setQueryData(['todos'], (old: Todo[] = []) => [ ...old, { ...newTodo, id: tempId }, ]); return { previousTodos, tempId };},onSuccess: (data, _variables, context) => { // 用服务端真实 ID 替换临时 ID queryClient.setQueryData(['todos'], (old: Todo[] = []) => old.map(todo => todo.id === context.tempId ? { ...todo, id: data.id } : todo ) );},并发冲突怎么处理?当多个乐观更新同时发生时,可能出现后一个覆盖前一个的情况。React Query 的推荐做法是依赖 onSettled 中的 invalidateQueries——每次 mutation 结束后都重新拉取最新数据,让 UI 最终收敛到服务器状态。如果对实时性要求更高,可以使用 queryClient.invalidateQueries 的 refetchType: 'all' 选项,确保所有相关查询立即刷新。优缺点对比优点:用户体验显著提升,操作即时反馈,无需等待网络往返减少感知延迟,即使在慢网络下 UI 也能快速响应接近原生应用的交互体验不需要手动管理 loading 和临时 UI 状态缺点:实现复杂度增加,需要正确处理回滚和缓存同步可能出现短暂的 UI 闪烁——用户先看到更新,失败后又回滚并发场景需要额外考虑冲突处理调试难度更高,问题可能出现在乐观写入、回滚或服务器同步任一环节什么时候该用?乐观更新最适合简单、可预测的操作:切换开关、编辑文本、点赞收藏。对于涉及复杂校验、金融计算或不可逆操作的场景,应该等待服务器确认后再更新 UI,避免误导用户。关键在于权衡:用户对即时反馈的期待,和操作失败时回滚带来的困惑,哪个影响更大。
前端阅读 05月27日 17:30

Qwik 和 React 有什么区别?

Qwik 和 React 的核心架构差异是什么?Qwik 和 React 最大的区别在于架构理念:React 基于 虚拟 DOM + 水合(Hydration),Qwik 基于 可恢复性(Resumability)+ 按需加载。这个根本差异直接影响了加载策略、状态管理、性能表现等方方面面。加载策略:全量下载 vs 按需加载React 在页面渲染时,通常需要下载整个应用包(或多个 chunk)。即使使用了 Code Splitting 做懒加载,也需要开发者手动配置:// React 懒加载需要手动配置const LazyComponent = React.lazy(() => import('./HeavyComponent'));function App() { return ( <Suspense fallback={<Loading />}> <LazyComponent /> </Suspense> );}Qwik 的加载策略完全不同——所有 JavaScript 默认都是延迟加载的,只有用户与页面交互时才加载和执行相关代码:// Qwik 组件:事件处理器自动延迟加载import { component$, useSignal } from '@builder.io/qwik';export default component$(() => { const count = useSignal(0); return ( <button onClick$={() => count.value++}> 点击了 {count.value} 次 </button> );});注意 Qwik 中的 component$ 和 onClick$,$ 后缀表示这是一个延迟加载边界,编译器会自动将这段代码拆分为独立 chunk,仅在需要时加载。水合 vs 可恢复性这是 Qwik 和 React 最本质的区别。React 的水合过程:SSR 渲染出 HTML 后,客户端必须重新下载并执行 JavaScript,重建组件树、附加事件监听器,让页面变得可交互。这个过程称为 Hydration:SSR HTML → 下载 JS → 执行组件代码 → 附加事件 → 页面可交互 ↑ 这一步耗时且昂贵即使用 React 18 的 Selective Hydration 做了部分优化,仍然无法避免大量 JavaScript 的下载和执行。Qwik 的可恢复性:不需要水合。Qwik 在 SSR 时将组件状态序列化到 HTML 中,事件监听器通过 HTML 属性直接附加:<!-- Qwik 渲染出的 HTML --><button on:click="/build/bundle-abc.js#handler_xyz"> 点击了 0 次</button>浏览器拿到 HTML 后,当用户点击按钮时,才去加载对应的 JS 函数并执行。页面天然就是可交互的,不需要任何"唤醒"过程:SSR HTML → 页面立即可交互 ↑ 无需额外 JS 执行状态管理:细粒度更新 vs 重新渲染React 使用 useState、useReducer、Context API 管理状态,状态变化会触发组件重新渲染:// React:状态更新触发组件重渲染function Counter() { const [count, setCount] = useState(0); // count 变化 → 整个组件重新执行 return <button onClick={() => setCount(c => c + 1)}>{count}</button>;}复杂应用中,开发者需要借助 useMemo、useCallback、React.memo 手动优化渲染性能,或者引入 Redux、Zustand 等外部状态管理库。Qwik 使用 useSignal 和 useStore 管理状态,状态变化只更新绑定的 DOM 节点,不会触发组件重新渲染:// Qwik:状态更新只更新具体 DOM 节点export default component$(() => { const count = useSignal(0); // count 变化 → 只更新 {count.value} 对应的文本节点 return <button onClick$={() => count.value++}>{count.value}</button>;});此外,Qwik 的状态会被序列化到 HTML 中,刷新页面后状态依然存在,不需要额外的状态恢复逻辑。性能数据对比| 指标 | React + Next.js | Qwik + Qwik City ||------|----------------|-------------------|| 首屏 JS 体积 | 40-100KB+ | 约 1-2KB || TTI(可交互时间) | 需等待水合完成 | HTML 加载即交互 || 水合开销 | 重新执行全部组件 JS | 无水合过程 || 代码分割 | 手动配置(lazy/Suspense) | 编译器自动完成 |Qwik 在首屏加载上的优势尤为明显——初始 JS 包只有 1-2KB,而 React 应用即使做了代码分割,首屏仍需加载框架核心和组件代码。开发体验对比React 的优势:生态系统成熟,npm 上几乎任何需求都有现成库可用。社区支持强大,遇到问题容易找到解决方案。Next.js 提供了完善的 SSR/SSG 方案。Qwik 的学习成本:语法与 React 相似(JSX、Hooks 风格的 API),但有几个关键差异需要适应:component$ 替代普通函数组件$ 后缀标记延迟加载边界useSignal / useStore 替代 useStateuseTask$ 替代 useEffect编译器自动处理优化,不需要手动写 useMemo / useCallback各自适合什么场景?选择 Qwik 的场景:内容密集型网站(博客、新闻、电商列表页)对首屏加载速度和 SEO 排名有严格要求面向移动端用户或网络条件不稳定的场景大型应用希望减少 JS 体积对性能的影响选择 React 的场景:需要丰富的第三方库和工具支持团队已有 React 经验,迁移成本需要考虑项目复杂度高,需要成熟的架构方案(如 Next.js App Router)快速原型开发,优先开发效率而非极致性能迁移建议如果你正在考虑从 React 迁移到 Qwik,需要注意:Qwik 提供了 qwik-react 集成,可以在 Qwik 应用中逐步引入 React 组件,支持渐进式迁移并非所有 React 生态库都有 Qwik 对应方案,复杂项目建议先做技术评估对于已有 React 项目,迁移优先级应基于性能瓶颈:如果当前应用首屏加载不是痛点,迁移的收益有限Qwik 通过可恢复性架构在首屏性能上建立了明显优势,但 React 凭借成熟的生态和社区仍是更稳妥的选择。具体选型应基于项目对性能、生态和团队能力的综合考量。
前端阅读 245月27日 01:12

React 组件渲染过程是怎么样的?

React 的渲染分两个阶段:Render 阶段(协调 Reconciliation):状态变化 → 创建新的 Virtual DOM 树 → 和旧的 Virtual DOM 做 diff → 标记需要更新的节点。这个阶段是纯计算,没有副作用,React 18 可以中断和恢复(并发模式)。Commit 阶段:把 diff 结果应用到真实 DOM。这个阶段不可中断,React 保证 DOM 更新的原子性。commit 结束后触发 useLayoutEffect 同步执行,然后浏览器重绘,最后异步执行 useEffect。React 18 之前 render 不可中断。18 的并发渲染(Concurrent Features)可以在 render 阶段暂停低优先级更新,优先处理用户交互等高优先级任务。追问什么情况下 React 会跳过组件的渲染?shouldComponentUpdate 返回 false(class 组件)React.memo 包裹的组件 props 没变(浅比较)state 没变化时(setState 传入相同值,React 用 Object.is 判断)Context value 没变时(但 Provider 下的所有 Consumer 会在 Provider render 时重新渲染,和 value 是否变化无关——这是常见性能陷阱)useLayoutEffect 和 useEffect 执行的时机有什么不同?useLayoutEffect 在 DOM 变更后、浏览器绘制前同步执行(阻塞渲染)。useEffect 在浏览器绘制后异步执行。需要读取 DOM 布局、同步更新防止闪烁的场景用 useLayoutEffect。React 18 并发模式的 "可中断渲染" 是怎么实现的?基于时间切片(Time Slicing)——Render 阶段被切成 5ms 的小段,每段结束后检查是否有更高优先级任务。如果有就暂停当前渲染,先处理高优任务。实现依赖的是 MessageChannel(而非 requestIdleCallback,因为后者在后台标签页可能被暂停)。
前端阅读 3302024年8月5日 12:52

React 如何做性能优化?有哪些常见手段?

React 在性能优化方面提供了多种策略和工具,以确保用户界面高效、平滑且响应迅速。以下是一些常用的性能优化手段:1. 使用 shouldComponentUpdate 和 React.PureComponent在类组件中,通过实现 shouldComponentUpdate 方法可以控制组件是否需要更新。当组件的状态或属性改变时,此方法会被调用,并根据返回的布尔值决定是否进行渲染。javascriptshouldComponentUpdate(nextProps, nextState) { // 只有当特定的属性或状态改变时才更新组件 return nextProps.id !== this.props.id || nextState.count !== this.state.count;}对于那些拥有不可变的属性和状态的组件,可以使用 React.PureComponent,它通过浅比较 props 和 state 来减少不必要的渲染。2. 使用 Hooks(如 React.memo 和 useMemo)对于函数组件,React.memo 是一个高阶组件,它仅在组件的 props 发生变化时才会重新渲染组件。javascriptconst MyComponent = React.memo(function MyComponent(props) { /* 只有props改变时,组件才会重新渲染 */});useMemo 和 useCallback 钩子可以用来缓存复杂计算的结果和回调函数,避免在每次渲染时都重新计算和创建新的函数实例。javascriptconst memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);const memoizedCallback = useCallback(() => { // 一个依赖特定props的回调函数}, [props]);3. 避免不必要的 DOM 更新当操作 DOM 时,应尽量减少更新次数和范围。可以使用虚拟列表(比如 react-window 或 react-virtualized)来仅渲染可视区域的元素,从而提高长列表的性能。4. 懒加载组件和路由使用 React.lazy 可以实现组件级别的代码拆分,这样可以将不同的组件打包成单独的代码块,并在需要时才加载它们。同时,结合 React Router 的 Suspense 组件,可以实现路由级别的懒加载,仅当路由被访问时才加载对应的组件。javascriptconst OtherComponent = React.lazy(() => import('./OtherComponent'));function MyComponent() { return ( React.Suspense fallback={div>Loading.../div>}> OtherComponent /> /React.Suspense> );}5. 使用 Web Workers对于复杂或计算密集型任务,可以使用 Web Workers 在后台线程中执行,避免阻塞主线程导致用户界面卡顿。6. 优化条件渲染避免不必要的渲染,例如,可以将条件渲染逻辑移到可能更改状态的事件处理函数中,而不是在渲染方法中进行。7. 状态升级将子组件的本地状态提升到父组件中,这样可以减少不必要的子组件渲染,因为状态的变化会集中处理。8. 使用不可变数据结构使用不可变数据可以更容易地检测到状态和属性的变化,这使得组件的更新检查更高效。库如 Immutable.js 可以用来帮助创建不可变数据。9. 使用生产版本的 React开发中通常使用的是开发版本的 React,它包含了许多有用的警告和错误信息。但在生产中,应该使用经过压缩和优化的生产版本,它删除了这些额外的警告和检查,以减少库的大小并提升性能。10. 分析和监控使用性能分析工具,如 React DevTools 中的 Profiler,可以帮助识别渲染性能瓶颈。它可以记录组件的渲染时间,并帮助你找到可以优化的部分。11. 避免内联对象和数组的传递对于那些接收对象或数组作为 props 的组件,应避免在渲染方法中直接创建新的内联对象或数组,因为这会导致 props 始终不相等,从而触发不必要的渲染。<MyComponent items={[1, 2, 3]} /> // 每次渲染都会创建一个新的数组,不推荐这样做// 更好的做法是在组件外部定义这个数组const items = [1, 2, 3];<MyComponent items={items} />12. 使用键(keys)来优化列表渲染当渲染列表时,应该为每个列表项指定一个唯一的 key。这有助于 React 确定哪些项已更改、添加或删除,从而提高列表渲染的效率。data.map((item) => <ListItem key={item.id} {...item} />)13. 使用 Context 时的优化当使用 React Context API 时,应该注意其可能对性能的影响。Context 的变动会导致所有消费该 Context 的组件重新渲染。为了避免不必要的渲染,可以分割 Context 或是使用 useMemo 跟 useCallback 来传递稳定的上下文值。14. 避免过度渲染和过度传递 props审视组件间的 props 传递,确保不会传递额外的 props。如果一个组件不需要某个 prop,那么就不应该传递它,因为这可能会导致不必要的组件渲染。15. 服务器端渲染 (SSR)服务器端渲染可以加快首次页面加载时间,并提升搜索引擎优化(SEO)。通过在服务器上生成 HTML,可以减少客户端的工作量,从而提升性能。实施示例假设我们有一个用户列表组件,其中包含大量用户数据。我们可以应用以下优化:使用 React.memo 封装用户列表项组件,仅在 props 变化时重新渲染。通过 useMemo 缓存用户列表计算,避免在每次渲染时重新计算。使用虚拟列表库,如 react-window,仅渲染可视区域内的用户,以优化长列表性能。如果用户列表是通过路由导航到达的,可以使用 React.lazy 和 Suspense 实现路由懒加载。通过 React DevTools 的 Profiler 分析用户列表的渲染性能,找出任何潜在的性能瓶颈进行优化。通过应用这些优化技巧,我们可以显著提高大型 React 应用的性能和用户体验。
前端阅读 1542024年8月5日 12:50

React hook 使用需要注意哪些?

在使用 React Hooks 时需要遵循一些最佳实践和注意事项,以确保代码的可维护性与功能的正确性。以下是一些关键点:1. 遵守Hooks规则不要在循环、条件或嵌套函数中调用HooksHooks 应该始终在组件的顶层被调用,这样可以保证 Hooks 在每次渲染时都以相同的顺序被调用,这对于 React 的内部状态追踪机制非常重要。只在React函数中调用Hooks应该仅在React的函数组件或自定义 Hooks 中调用 Hooks。不要在普通的 JavaScript 函数中调用。2. 使用 useState时的注意事项初始化状态对于复杂的状态逻辑,可以通过传递一个函数给 useState 来惰性初始化,这样可以避免在每次渲染时重新创建初始状态。const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState;});状态更新函数的身份稳定setState 函数是身份稳定的,这意味着你可以在其他 Hooks 中安全地引用它,而不用担心它会在重新渲染时改变。3. 使用 useEffect时的注意事项清理副作用在 useEffect 中创建的订阅、定时器、监听事件等副作用,应该在返回的清理函数中进行清除,以避免内存泄漏。useEffect(() => { const subscription = props.source.subscribe(); return () => { // 清理订阅 subscription.unsubscribe(); };}, [props.source]);依赖列表的完整性确保依赖列表包含了所有外部作用域中被 useEffect 使用到的值,这样才能正确响应这些值的变化。如果忽略了依赖,可能会导致旧的闭包中的值被捕获,从而引发错误。useEffect(() => { function doSomething() { console.log(someProp); } doSomething();}, [someProp]); // 确保所有使用到的变量都被包含在依赖列表中4. 避免在 useEffect中进行不必要的操作节流和防抖如果 useEffect 中的操作非常昂贵,考虑使用节流(throttling)或防抖(debouncing)技术来减少操作的频率。5. 自定义Hooks代码复用当你发现需要在不同组件之间复用状态逻辑时,可以将其抽离成自定义 Hooks。这有助于减少代码冗余并增强逻辑的可维护性。例如,使用自定义 useForm Hook 来处理表单:function useForm(initialValues) { const [values, setValues] = useState(initialValues); const handleChange = (event) => { setValues({ ...values, [event.target.name]: event.target.value, }); }; return [values, handleChange];}6. 性能优化useMemo 和 useCallback在有必要的情况下,使用 useMemo 和 useCallback 来避免不必要的渲染或计算。useMemo 可以用来缓存复杂计算的结果,useCallback 可以用来缓存函数,这在将函数传递给子组件时特别有用,可以避免不必要的子组件重渲染。
前端阅读 1472024年8月5日 12:48

什么是 React 的受控组件和非受控组件?

在React中,受控组件(Controlled Components)和非受控组件(Uncontrolled Components)都是处理表单输入的方式,但它们处理数据的方式不同。受控组件(Controlled Components)受控组件是React的一种模式,在这种模式下,表单数据是由React组件的状态管理的。这意味着每次字段的值发生变化时,我们都会通过一个事件处理函数(通常是 onChange)来更新组件的状态。然后,组件的状态被用作输入字段的值,确保组件的状态是数据的唯一来源。示例:假设我们有一个受控的 <input>元素:class ControlledComponent extends React.Component { constructor(props) { super(props); this.state = {value: ''}; } handleChange = (event) => { this.setState({value: event.target.value}); } render() { return ( <form> <label> Name: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> </form> ); }}在上面的例子中,<input>的值始终由 this.state.value决定,而且每当用户输入时,handleChange函数都会被调用,更新状态,因此界面显示的内容总是和状态同步。非受控组件(Uncontrolled Components)非受控组件是另一种模式,在这种模式下,表单数据是由DOM本身处理的,而不是由React状态管理。这就像传统的HTML表单工作方式。在非受控组件中,我们通常使用 ref来从DOM节点获取表单数据,而不是为每个状态变化编写事件处理函数。示例:下面是一个非受控组件的例子:class UncontrolledComponent extends React.Component { constructor(props) { super(props); this.inputRef = React.createRef(); } handleSubmit = (event) => { alert('A name was submitted: ' + this.inputRef.current.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Name: <input type="text" ref={this.inputRef} /> </label> <button type="submit">Submit</button> </form> ); }}在上面的例子中,<input>不是通过状态来控制其值,而是通过 ref来访问DOM节点获取其值。 总结受控组件允许你更好地控制表单的行为,因为组件的状态充当了数据的真实来源。非受控组件舍弃了对表单状态的即时控制,使得组件的代码更简洁,但可能会更难管理表单的状态,尤其是在复杂的表单交互时。在实际的开发实践中,受控组件通常是首选方法,因为它们更加符合React的数据流概念,使得状态的管理更加清晰和可预测。然而,对于一些简单的表单或者集成第三方DOM库时,非受控组件也可能是一个不错的选择。
前端阅读 2712024年8月5日 12:48

React 如何使用异步组件以及异步组件的使用场景

React 的异步组件(通常被称为懒加载组件)主要是通过动态 import() 语法和 React 的 React.lazy 函数来实现的。它们用于在需要时才加载组件,可以显著提高应用程序的性能,尤其是当应用程序很大并且有许多不同的页面和组件时。接下来,我会详细介绍如何使用异步组件以及它们的使用场景。 如何使用异步组件使用 React 异步组件的基本步骤如下:使用 React.lazy 函数分别导入组件。这个函数允许你定义一个动态导入的组件。该函数接受一个函数,这个函数必须调用一个 import(),它返回一个 Promise,该 Promise 解析为一个有 default 导出的模块。 const AsyncComponent = React.lazy(() => import('./AsyncComponent'));将 React.lazy 返回的组件与 React.Suspense 组件结合使用。Suspense 组件允许你指定加载指示器(例如:加载中的旋转器),在等待异步组件加载时显示给用户。 import React, { Suspense } from 'react'; // 异步导入组件 const AsyncComponent = React.lazy(() => import('./AsyncComponent')); function App() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <AsyncComponent /> </Suspense> </div> ); }使用场景性能优化: 对于大型应用程序,将不同的页面或功能分割成独立的代码块,然后只在用户需要时才加载,可以减少应用程序的初始负载时间。条件渲染组件: 当一个组件只在某些条件下才需要时,例如特定的用户角色或权限,可以使用异步组件按需加载,从而节省资源。路由懒加载: 在使用如 React Router 这样的库进行路由管理时,可以结合 React.lazy 和 Suspense 来实现路由级别的懒加载。 import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import React, { Suspense } from 'react'; const Home = React.lazy(() => import('./Home')); const About = React.lazy(() => import('./About')); const App = () => ( <Router> <Suspense fallback={<div>Loading...</div>}> <Switch> <Route exact path="/" component={Home} /> <Route path="/about" component={About} /> </Switch> </Suspense> </Router> );组件库懒加载: 如果你的应用程序使用了庞大的第三方组件库,而只有少数组件被频繁使用,可以选择仅懒加载那些较少使用的组件,以减少初始包的大小。使用异步组件的主要目标是提升用户体验,减少页面加载时间,并且按需加载资源,避免浪费客户端的计算和带宽资源。React 的懒加载功能是实现上述目标的重要手段之一。
前端阅读 3312024年8月5日 12:48

React Router 是如何配置组件的懒加载?

React Router 可以通过配合 React 的 React.lazy() 和 Suspense 组件来配置组件的懒加载。以下是使用 React Router 实现懒加载的基本步骤:使用 React.lazy 实现动态导入: React.lazy() 是一个允许你动态加载组件的函数。它可以让你定义一个动态导入的组件,并且这个组件会在首次渲染时自动加载。 const LazyComponent = React.lazy(() => import('./LazyComponent'));使用 Suspense 组件包裹路由: 在你的应用中,你需要使用 Suspense 组件来包裹懒加载的路由。Suspense 可以指定一个加载指示器(比如一个 spinner),它会在懒加载组件加载完成之前显示。 import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import React, { Suspense } from 'react'; const LazyComponent = React.lazy(() => import('./LazyComponent')); function App() { return ( <Router> <Suspense fallback={<div>Loading...</div>}> <Switch> <Route path="/lazy" component={LazyComponent} /> {/* 其他路由 */} </Switch> </Suspense> </Router> ); }为懒加载组件创建独立的 chunk: 当你使用 create-react-app 或其他构建工具时,它会为每个用 React.lazy() 引入的组件自动创建一个独立的 JavaScript chunk 文件。这意味着这些代码只会在用户需要时才会被加载。举个例子,假设你有一个很大的组件 BigComponent,你不希望它在应用首次加载时就加载进来,而是希望当用户真正访问到该组件对应的路由时再加载,你可以这样设置:import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';import React, { Suspense } from 'react';const BigComponent = React.lazy(() => import('./BigComponent'));function App() { return ( <Router> <Suspense fallback={<div>Loading Big Component...</div>}> <Switch> <Route path="/big-component" component={BigComponent} /> {/* 其他路由 */} </Switch> </Suspense> </Router> );}在上述例子中,当用户访问 /big-component 路径时,BigComponent 会被动态加载。用户会看到 "Loading Big Component…" 的文本,直到 BigComponent 加载完成并准备好渲染。这样可以减少应用的初始加载时间,并且按需加载资源,提高性能。
前端阅读 1512024年8月5日 12:43

React 函数组件和 class 组件之间的区别

React 函数组件和类组件是 React 中创建组件的两种不同方式。它们有几个主要区别:语法:函数组件:使用 JavaScript 函数(或箭头函数)定义,它接收一个 props 参数并返回 JSX。函数组件通常更简洁。 jsx function Welcome(props) { return <h1>Hello, {props.name}</h1>; }类组件:使用 ES6 类来定义,它扩展自 React.Component,必须包含一个 render() 方法,该方法返回 JSX。 jsx class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }状态管理:函数组件:在 React 16.8 之前,函数组件不具备状态(state)和生命周期方法。但随着 React Hooks 的引入,函数组件可以使用 useState 和其他 Hooks 来管理状态和生命周期。类组件:具有内置的状态和生命周期方法。它们使用 this.state 和 this.setState 来管理组件的状态,以及一系列的生命周期函数(如 componentDidMount,componentShouldUpdate 等)来执行副作用操作。生命周期方法:函数组件:通过使用 React Hooks(如 useEffect),函数组件可以执行与类组件生命周期方法相似的操作,但它们不直接拥有生命周期方法。类组件:具有完整的生命周期方法,可以在组件的不同阶段执行代码。this 关键字:函数组件:不使用 this 关键字。所有的数据(包括 props 和 state)都通过函数参数或 Hooks 访问。类组件:需要使用 this 关键字来访问 props、state 和类方法。优化性能:函数组件:因为它们没有类实例,理论上可以更轻量。并且可以通过使用 React.memo 进行性能优化。类组件:可以使用 shouldComponentUpdate 或 PureComponent 来优化性能,但这些通常比函数组件中的优化方法更复杂。钩子(Hooks):函数组件:可以使用 Hooks,如 useState、useEffect 等,使得在不使用类的情况下也能拥有类似的功能。类组件:无法使用 Hooks,必须依靠类本身的特性和生命周期。部署和维护:函数组件:通常来说,由于它们更加简洁,函数组件更容易编写和维护。它们也更容易分割成更小的函数。类组件:可能会更加冗长,特别是当涉及到多个生命周期方法和状态管理时,这可能使得维护和重构变得更加困难。代码复用:函数组件:可以通过自定义 Hooks 实现逻辑的复用。类组件:通常通过高阶组件(HOCs)或渲染道具(Render Props)来实现逻辑的复用。
前端阅读 1192024年8月5日 12:43

React 的合成事件的原理是什么?

React 的合成事件(SyntheticEvent)是 React 为了跨浏览器兼容性而实现的一个事件封装。合成事件的原理可以总结为以下几个关键点:1. 事件封装React 为了解决原生事件在不同浏览器中的兼容问题,实现了一套自己的事件系统。这个系统模拟原生事件系统,但是提供了一致的接口和行为。当事件发生时(如用户点击一个按钮),React 会创建一个合成事件的实例,这个实例包含了所有事件的信息,无论在哪个浏览器上。2. 事件冒泡在 React 中,所有的事件都会自动应用事件冒泡(bubbling),即事件会从触发它的最深的节点开始,逐层向上传播到最外层的节点。合成事件同样遵循这个机制,这意味着您只需要在一个高层节点上监听事件,就能处理下层节点的相应事件。3. 事件委托React 并不会将事件处理器直接绑定到真实的 DOM 元素上,而是使用了一种叫做事件委托的技术。React 在最顶层的文档节点上(通常是document)添加了一个单一的事件监听器,用来监听所有支持的事件类型。当一个事件发生时,React 会根据这个事件的目标和冒泡路径,来决定哪些注册的事件处理器需要被调用。4. 合成事件对象池出于性能考虑,React 实现了一个合成事件的对象池。每当一个事件发生并且事件处理器被调用时,React 从池中分配一个合成事件对象,并填充事件的相关信息。一旦事件处理器被调用,这个对象会被清空,并回收到池中以供后续的事件重复使用。这个过程减少了垃圾回收的压力和合成事件对象的创建成本。5. 与原生事件的关系尽管 React 使用了合成事件,但它仍然是基于原生事件的。当原生事件被触发时,React 的事件委托机制会处理这个事件,并创建一个合成事件传递给相应的事件处理器。开发者在编写事件处理函数时,操作的是由 React 提供的合成事件,而不是直接操作原生 DOM 事件。实例假设我们有一个按钮的点击事件,我们希望在点击时打印出事件对象:class MyComponent extends React.Component { handleClick = (event) => { console.log(event); // 这里的 event 是一个 SyntheticEvent 的实例 console.log(event.nativeEvent); // 这里可以访问原生的 DOM 事件对象 }; render() { return <button onClick={this.handleClick}>Click me</button>; }}这段代码中,handleClick 方法接收一个合成事件的实例。我们可以访问此对象的所有属性和方法,如同处理一个原生事件一样。但由于它是合成的,它在所有浏览器中的行为都是一致的。综上所述,React 的合成事件系统提供了一种高效且一致的方式来处理浏览器间的事件差异,并优化了性能,同时简化了事件处理的复杂性。
前端阅读 1362024年8月5日 12:43

React 的 vdom 是什么?以及虚拟DOM 是如何做 diff 算法的?

React 的虚拟DOM(VDOM)是React用于提升应用性能的核心概念之一。它是对真实DOM的一个轻量级抽象。虚拟DOM本质上是一个JavaScript对象,它是真实DOM结构的一个简化版本。React使用虚拟DOM来模拟真实DOM的更新,这样就可以最小化对真实DOM的操作,因为真实DOM操作的开销通常比较大。当组件的状态变化时,React会创建一个新的虚拟DOM树并将其与上一次的虚拟DOM树进行比较。这个过程被称为Diff算法。通过Diff算法,React可以确定实际DOM需要进行的最小更新。以下是Diff算法的简要步骤:树的比较:React首先比较两棵树的根节点,如果根节点的类型不同(例如从<div>变到<span>),React会销毁旧树并建立一棵新树。如果类型相同,则保留根节点,并继续进行递归比较。组件的比较:如果是React组件节点,React会检查组件的类型是否相同。如果相同,组件将接收新的props并重新渲染。然后,React会比较返回的虚拟DOM。子元素的比较:当比较两个相同类型的元素时,React会继续比较它们的子元素。React有两种不同的策略来比较子元素:同层比较:React只比较同一层级的子元素。如果在不同层级有相同的元素,React不会尝试复用这些元素。key属性:当开发者提供了key属性时,React会使用这个key来匹配旧的虚拟DOM树中的元素和新的虚拟DOM树中的元素。这有助于保持状态和提高性能,特别是在处理列表时。更新DOM:一旦Diff算法确定了需要变更的最小部分,React会批量执行这些更新,尽量减少对真实DOM的操作,从而提高性能。例子:假设有一个列表,列表项组件<ListItem />有一个唯一的key属性,并且列表的状态更新导致列表项的顺序颠倒。由于每个<ListItem />都有唯一的key,React能够识别出这些组件只是顺序改变了,而不是完全不同的组件。因此,React仅会改变DOM中这些列表项的顺序,而不是销毁整个列表并重新创建,这大大提高了性能。总结:React的虚拟DOM和Diff算法共同工作,以提供高效的更新机制。虚拟DOM使得React可以在内存中进行计算,而Diff算法确保只对真实DOM做必要的、最小的修改。这种机制使得React在处理大型、动态的应用时能够保持良好的性能。
前端阅读 1432024年7月18日 00:30

说明元素和组件之间的区别?

在软件工程中,元素和组件是两个关键概念,它们在构建应用程序时发挥着不同的作用。元素 通常指的是构成界面的基本单元,它可以是HTML中的一个标签,如一个按钮、一个输入框或者一个图片等。在某些框架中,如React,元素描述了你想在屏幕上看到的内容。元素是不可变的,一旦被创建,你不能改变其子元素或属性。一个元素就像一个单纯的说明书,它告诉框架应该如何构建视图。组件 则是更高级的概念,它封装了元素以及与之相关的逻辑。组件可以包含一个或多个元素,并且通常会包含一些内部状态或者行为,例如按钮的点击事件处理。组件可以是可复用的,且可以嵌套使用,构建复杂的UI结构。在许多现代前端框架中,如React, Vue, Angular等,组件是构建应用程序的基本单元。简单来说,元素是静态的、不可变的描述,而组件是动态的、可复用的封装,包含了逻辑和界面。
前端阅读 1102024年7月15日 23:41

useState Hook是如何工作的?

useState 是 React Hook 中的一个函数,它允许你在函数组件中添加状态。这是一个基本的 Hook,用于在不编写类组件的情况下使用 state。当你调用 useState 时,你需要传递初始状态给它,这可以是任何数据类型,比如数字、字符串、数组或对象等。useState 函数会返回一个包含两个元素的数组:当前状态值和一个更新该状态的函数。例如,如果你想在组件中使用一个计数器,你可以这样写:const [count, setCount] = useState(0);在这里,0 是 count 的初始值。setCount 是一个函数,当你调用它并传递一个新的值时,它会更新 count 的值,并且组件会重新渲染以反映更新后的状态。React 保证在多次渲染之间保持 useState 提供的状态和更新函数的稳定性,这意味着你不需要担心状态在重新渲染时会丢失或错误地更新。此外,每次组件渲染时,useState 返回的状态值总是最新的。这样,你可以在事件处理器或其他函数中安全地使用这些状态值,而不用担心它们是过时的。