当前位置: 首页 > news >正文

前端梳理体系从常问问题去完善-框架篇(react生态)

前言

国庆去趟了杭州,但是人太多了,走路都觉得空气很闷,天气也很热,玩了两天就回宿舍躺了,感觉人太多,看不到风景,而且消费也很高,性价比不是很值得,就呆在公寓,看了两本书,有一本是名著,《呼啸山庄》虽然是写的是爱情,但爱情背后是人性。爱情啊,这个课题本来就是让人很难读懂得,关于爱,也看了一篇文章。关于爱上人渣得,爱上人渣,或是那些求而不得甚至是受制于禁忌的爱,本质上也是在追求这种刺激,或者说正是因为这样的对象能给自己麻木的感官更大的刺激,从而误以为这就是「爱」的本质,就像是人们虽然知道「吊桥效应」,但在当下那种面红耳赤心跳加速的感官,其实是很难理性地进行拆解和分析。

青楼女子最会撩人的不是她会多少曲谱、会多少诗词、会多么的千杯不倒,而是在当下故意弹错的那一弦,让那个心念已久的小情郎抬头与自己看似不经意地四目交接。

好吧回归正题,也整理一下,react生态得相关问题,跟知识点。

React

Hooks

常见的hooks用法以及使用

1. 基础状态与副作用 Hooks

  • useState:管理组件内部状态,支持多次调用管理多个独立状态。
    • 用法:const [state, setState] = useState(initialValue)
    • 特点:状态更新异步,依赖旧状态时用函数形式(setState(prev => newVal))。
  • useEffect:处理组件副作用(数据请求、订阅、DOM 操作等)。
    • 用法:useEffect(effectFn, dependencies)
    • 特点:依赖数组控制执行时机,返回清理函数处理卸载逻辑,空数组 [] 仅执行一次。

2. 状态与逻辑管理 Hooks

  • useReducer:管理复杂状态逻辑,替代 useState 处理多状态关联场景。
    • 用法:const [state, dispatch] = useReducer(reducer, initialState)
    • 适用场景:表单处理、多步骤状态切换等复杂状态逻辑。
  • useContext:跨组件共享状态,避免 props 层层传递。
    • 用法:const value = useContext(Context)
    • 前提:需配合 createContext 创建上下文和 Context.Provider 提供值。

3. 性能优化 Hooks

  • useCallback:记忆化函数,避免子组件因函数引用变化频繁重渲染。
    • 用法:const memoizedFn = useCallback(fn, dependencies)
    • 适用场景:传递给子组件的回调函数(如 onClick)。
  • useMemo:记忆化计算结果,避免每次渲染重复执行耗时计算。
    • 用法:const memoizedValue = useMemo(calculateFn, dependencies)
    • 注意:用于计算密集型操作,不滥用(避免额外性能开销)。

4. 引用与 DOM 操作 Hooks

  • useRef

    :在多次渲染间保存值,或直接访问 DOM 元素。

    • 用法:const ref = useRef(initialValue)
    • 特点:修改 ref.current 不触发重渲染,可用于存储计时器、DOM 引用等。

5. 自定义 Hooks

  • 核心作用:封装可复用的组件逻辑,抽离重复代码。
  • 命名规则:必须以 use 开头(如 useWindowSizeuseFetch)。
  • 示例场景:封装数据请求逻辑、监听窗口大小、表单验证等。

6. Hooks 使用规则

  • 只能在函数组件顶层自定义 Hooks 中调用,不可在条件、循环、嵌套函数中使用。
  • 每次组件渲染时,Hooks 调用顺序必须保持一致
  • 自定义 Hooks 需遵循命名规范,确保 React 正确识别。

7. 适用场景速查表

Hook核心场景解决问题
useState简单状态管理函数组件无状态的问题
useEffect副作用处理生命周期逻辑分散的问题
useReducer复杂状态逻辑多状态关联、状态更新逻辑复杂
useContext跨组件状态共享props 透传繁琐问题
useCallback优化子组件渲染函数引用频繁变化导致重渲染
useMemo优化计算性能重复执行耗时计算
useRef保存跨渲染值或操作 DOM获取 DOM 元素、存储不触发重渲染的值
useCallBack与useMemo怎么做得缓存?

useCallbackuseMemo 的缓存是存储在 组件实例的内部状态 中的,由 React 内部机制管理。

具体来说,当组件首次渲染时,React 会为组件实例创建一块专属的 “内存空间”,用于存储该组件中所有 Hook(包括 useStateuseEffectuseCallbackuseMemo 等)的状态和缓存信息。

  • 对于 useCallback:缓存的函数引用会被存储在这块空间中,与组件实例绑定。
  • 对于 useMemo:缓存的计算结果也会被存储在这块空间中,同样与组件实例绑定。

几个关键特点:

  1. 与组件实例绑定:每个组件实例(比如同一个组件渲染多次,会产生多个实例)都有自己独立的缓存空间,互不干扰。例如,两个 Parent 组件实例,它们的 useCallback 缓存的函数是各自独立的。
  2. 随组件生命周期存在:当组件被卸载时,其对应的缓存空间会被 React 自动清理,不会造成内存泄漏。
  3. 依赖项驱动更新:每次组件重新渲染时,React 会检查 useCallback/useMemo 的依赖项数组。如果依赖项没变,就直接复用缓存中的值;如果依赖项变了,就更新缓存并存储新的值。

简单说,缓存既不是全局变量,也不是浏览器的本地存储(如 localStorage),而是 React 为每个组件实例在内存中维护的一块临时存储区域,专门用于优化组件渲染性能。

useCallBack与useMemo页面销毁得时候怎么做得清理?

useCallbackuseMemo 本身并不需要手动清理缓存,因为它们的缓存会随着组件实例的销毁而自动被清理,具体机制如下:

1. 缓存的生命周期与组件实例绑定

useCallbackuseMemo 的缓存数据(函数引用或计算结果)是存储在组件实例的内部状态中的,完全依附于组件实例的生命周期:

  • 当组件首次渲染时,React 为该组件创建一个实例,并在实例中分配空间存储这些缓存。
  • 当组件被销毁(从 DOM 中移除)时,整个组件实例会被 React 标记为 “可回收”。

2. 清理依赖 JavaScript 垃圾回收(GC)

当组件实例被销毁后,它所占用的内存(包括 useCallback/useMemo 的缓存)会成为 JavaScript 垃圾回收机制的目标:

  • 由于组件实例已不再被任何引用(比如父组件不再渲染它,没有其他变量持有它的引用),垃圾回收器会自动识别并释放这部分内存。
  • 缓存数据本身不涉及 “副作用”(如事件监听、定时器、网络请求等),因此不需要像 useEffect 那样手动编写清理函数(如移除事件监听)。

3. 为什么不需要手动清理?

useCallbackuseMemo 的核心是 “缓存值”,而不是 “副作用”:

  • 它们不产生需要手动终止的外部资源占用(如 DOM 事件、定时器 ID 等)。
  • 缓存的本质是内存中的临时数据,当宿主(组件实例)被销毁后,这些数据自然失去存在意义,会被自动回收。

总结

useCallbackuseMemo 的缓存清理是自动完成的:
组件销毁 → 组件实例被标记为可回收 → JavaScript 垃圾回收器释放实例占用的内存(包括缓存数据)。
开发者无需手动干预,也没有专门的 API 用于清理它们的缓存。

useCallBack与useMemo的使用场景以及区别

可能存在的误区包括:何时使用哪一个,是否所有函数都需要用useCallback包裹,或者是否所有计算都需要useMemo。需要指出不必要的使用可能导致性能下降,而不是提升。

在 React 中,useCallbackuseMemo 是性能优化工具,但并非所有场景都需要使用。以下是使用指南和最佳实践:

1. useCallback 的适用场景*

useCallback 用于缓存函数引用,避免因函数重新创建导致子组件不必要的重新渲染。仅在以下情况使用

  • 传递给依赖引用相等性的子组件(如使用 React.memo 的组件)。
  • 作为依赖项传递给其他 Hook(如 useEffectuseMemo)。

示例:优化子组件渲染

// 子组件使用 React.memo 浅比较 props
const ChildComponent = React.memo(({ onClick }) => {return <button onClick={onClick}>Click</button>;
});// 父组件使用 useCallback 缓存函数
function Parent() {const [count, setCount] = useState(0);// ✅ 只有依赖项变化时才重新创建函数const handleClick = useCallback(() => {setCount(count + 1);}, [count]); // 依赖 count,必须包含在依赖数组中return <ChildComponent onClick={handleClick} />;
}

错误用法(不要过度使用)

function Parent() {// ❌ 不必要的 useCallback:函数未传递给子组件const handleClick = useCallback(() => {console.log('Clicked');}, []);return <button onClick={handleClick}>Click</button>;
}

2. useMemo 的适用场景

useMemo 用于缓存计算结果,避免重复执行高开销的计算。仅在以下情况使用

  • 计算成本高昂(如大量数据排序、复杂计算)。
  • 对象 / 数组作为 props 传递给依赖引用相等性的子组件

示例:缓存昂贵计算

function Example({ items }) {// ✅ 只有 items 变化时才重新排序const sortedItems = useMemo(() => {return [...items].sort((a, b) => a.name.localeCompare(b.name));}, [items]);return <List items={sortedItems} />;
}

示例:避免子组件不必要的重新渲染

const Child = React.memo(({ style }) => {return <div style={style}>Child</div>;
});function Parent() {const [count, setCount] = useState(0);// ✅ 只有 count 变化时才重新创建 style 对象const style = useMemo(() => {return { color: count > 5 ? 'red' : 'blue' };}, [count]);return <Child style={style} />;
}

3. 何时不使用?

不要用 useCallback 包裹所有函数

  • 函数未传递给子组件:普通事件处理函数不需要缓存。
  • 依赖频繁变化:若依赖数组中的值频繁更新,useCallback 可能导致更多性能开销。

不要用 useMemo 缓存所有计算

  • 计算成本低:简单计算(如 a + b)不需要缓存。
  • 每次渲染必须执行:如直接操作 DOM、生成随机数等。
function Example() {// ❌ 不必要的 useMemo:计算成本低const sum = useMemo(() => a + b, [a, b]);// ✅ 直接计算更高效const sum = a + b;return <div>{sum}</div>;
}

4. 性能反模式*

过度依赖引用相等性

过度使用 React.memouseCallbackuseMemo 可能导致:

  • 代码复杂度增加:依赖数组管理困难。
  • 内存占用增加:缓存大量函数 / 对象。
  • 性能下降:比较依赖项的开销可能超过重新计算的成本。

错误的依赖数组

  • 遗漏依赖项:导致闭包陷阱(如 useEffect 中使用旧值)。
  • 包含不必要的依赖:导致函数 / 计算频繁重新创建。

5. 最佳实践

  1. 优先编写清晰的代码,而非追求性能优化。
  2. 使用 Profiler 工具(如 React DevTools)定位真正的性能瓶颈。
  3. 仅在必要时使用 useCallbackuseMemo,并确保依赖数组正确。
  4. 避免过早优化:大多数情况下,React 的默认渲染性能已经足够好。

总结

场景是否使用
普通事件处理函数❌ 不需要
传递给 React.memo 的函数✅ 需要
高开销的计算✅ 需要
简单计算❌ 不需要
对象 / 数组作为 props 传递✅ 推荐使用

记住:优化的目标是减少不必要的工作,而非避免所有工作

父组件渲染是否引起子组件的渲染

在 React 中,父组件的渲染并不总是导致子组件的渲染,这取决于以下因素:

  1. 默认行为:父组件渲染会触发子组件渲染

React 的默认行为是:当父组件重新渲染时,其所有子组件都会重新渲染(无论 props 是否变化)。这是因为 React 会递归比较虚拟 DOM 树。

function Parent() {const [count, setCount] = useState(0);// 每次 Parent 渲染时,Child 都会被重新渲染return <Child message="Hello" />;
}function Child({ message }) {console.log("Child rendered"); // 每次父组件渲染时都会执行return <div>{message}</div>;
}
  1. 使用 React.memo 避免不必要的渲染

React.memo 是一个高阶组件,它会浅比较子组件的 props。只有当 props 发生变化时,子组件才会重新渲染。

// ✅ Child 只会在 props 变化时重新渲染
const Child = React.memo(({ message }) => {console.log("Child rendered");return <div>{message}</div>;
});

注意事项

  • 浅比较的局限性

    React.memo
    

    仅检查 props 的引用是否变化。如果传递对象 / 数组 / 函数,即使内容相同,引用变化也会触发重新渲染。

    function Parent() {const [count, setCount] = useState(0);// ❌ 每次渲染都会创建新的对象,导致 Child 重新渲染
    const data = { value: "static" };return <Child data={data} />;
    }
    
### 3. **函数 / 对象引用变化导致的渲染**如果父组件在每次渲染时创建新的函数、对象或数组,即使内容相同,也会导致子组件重新渲染(即使使用了 `React.memo`)。```jsx
function Parent() {const [count, setCount] = useState(0);// ❌ 每次渲染都会创建新的函数const handleClick = () => {console.log("Clicked");};return <Child onClick={handleClick} />; // Child 会重新渲染
}// 即使使用 React.memo,onClick 的引用变化仍会触发渲染
const Child = React.memo(({ onClick }) => {return <button onClick={onClick}>Click</button>;
});

解决方案:使用 useCallback 缓存函数引用。

// ✅ 使用 useCallback 确保函数引用不变
const handleClick = useCallback(() => {console.log("Clicked");
}, []); // 空依赖数组表示只创建一次函数
  1. 状态提升与上下文导致的渲染

如果多个组件共享上层组件的状态(如状态提升或 Context API),上层组件的状态更新会导致所有依赖该状态的子组件重新渲染。

// 使用 Context API 时
const MyContext = React.createContext();function Parent() {const [count, setCount] = useState(0);return (<MyContext.Provider value={count}><Child1 /> {/* 依赖 count,会重新渲染 */}<Child2 /> {/* 不依赖 count,但仍会重新渲染(默认行为) */}</MyContext.Provider>);
}

优化方法

  • 将不依赖状态的子组件移出提供者。
  • 使用多个 Context 分离关注点。
  1. 强制子组件重新渲染

即使使用 React.memo,也可以通过传递唯一的 key 来强制子组件重新渲染。

function Parent() {const [count, setCount] = useState(0);// 每次 count 变化时,Child 都会重新创建(而非更新)return <Child key={count} message="Hello" />;
}

总结:父组件渲染对子组件的影响

条件子组件是否重新渲染
子组件未使用 React.memo✅ 总是重新渲染
子组件使用 React.memo,且 props 未变化❌ 不重新渲染
子组件使用 React.memo,但 props 中的对象 / 函数引用变化✅ 重新渲染
父组件通过 key 强制更新✅ 重新渲染

性能优化建议

  • 使用 React.memo 包裹纯组件。
  • 使用 useCallbackuseMemo 避免不必要的引用变化。
  • 使用 Profiler 工具定位渲染瓶颈。
useEffect与useLayoutEffect的区别

在 React 中,useEffectuseLayoutEffect 是用于处理副作用的 Hooks,但它们的执行时机和适用场景有重要区别。

1. useEffect(默认副作用)

  • 执行时机:在浏览器完成渲染后(视觉更新已完成),异步执行。
  • 特点:
    • 不会阻塞页面渲染,适合处理不影响视觉的副作用(如数据获取、订阅、日志)。
    • 可能在多次渲染后合并执行(如在浏览器空闲时)。

示例

useEffect(() => {// 在页面渲染完成后执行console.log('Effect 执行');return () => {// 清理函数在组件卸载前或下次 effect 执行前调用console.log('Effect 清理');};
}, []); // 空依赖数组表示只在挂载和卸载时执行

2. useLayoutEffect(布局副作用)

  • 执行时机:在浏览器完成 DOM 更新但尚未绘制到屏幕前,同步执行。

  • 特点:

    • 会阻塞页面渲染,适合需要读取 DOM 布局并立即更新的场景(如测量元素尺寸、滚动位置)。
  • 总是在 useEffect 之前执行。

示例

useLayoutEffect(() => {// 在 DOM 更新后、绘制前执行const width = elementRef.current.offsetWidth;console.log('元素宽度:', width);// 可以同步更新 DOM(如调整样式)elementRef.current.style.opacity = 0.8;return () => {// 清理函数在组件卸载前或下次 effect 执行前调用console.log('LayoutEffect 清理');};
}, []);

3. 核心区别对比

特性useEffectuseLayoutEffect
执行时机渲染后(异步)渲染前(同步)
是否阻塞渲染❌ 否✅ 是
适用场景数据获取、订阅、日志DOM 测量、布局调整、同步 DOM 更新
性能影响低(不阻塞用户界面)高(可能导致视觉闪烁)

4. 何时使用哪个?

优先使用 useEffect

  • 大多数副作用场景(如网络请求、设置定时器)。
  • 不依赖 DOM 布局的操作。

使用 useLayoutEffect 的情况

  • 需要读取 DOM 布局信息(如元素尺寸、滚动位置)。
  • 需要在用户看到更新前同步修改 DOM(如避免视觉闪烁)。

示例:避免视觉闪烁

function App() {const [width, setWidth] = useState(0);const ref = useRef(null);// ❌ 使用 useEffect 可能导致闪烁(先显示默认值,再更新)useEffect(() => {const w = ref.current.offsetWidth;setWidth(w); // 会触发额外渲染}, []);// ✅ 使用 useLayoutEffect 可避免闪烁(在绘制前更新)useLayoutEffect(() => {const w = ref.current.offsetWidth;setWidth(w); // 同步更新,用户看不到中间状态}, []);return <div ref={ref} style={{ width: `${width}px` }}>Hello</div>;
}

5. 性能注意事项

  • 避免在 useLayoutEffect 中执行耗时操作,否则会阻塞页面渲染,导致卡顿。
  • 如果副作用不需要操作 DOM,始终使用 useEffect 以保持最佳性能。

总结

场景推荐 Hook
数据获取、订阅useEffect
DOM 测量、同步样式更新useLayoutEffect
避免视觉闪烁useLayoutEffect
优化性能优先使用 useEffect

记住:useLayoutEffectuseEffect 的同步版本,仅在必要时使用

为什么不能在条件语句中进行使用

React Hooks 不能在条件语句中执行(即使条件 “一直为真”),核心原因与 React 内部对 Hooks 的状态管理机制调用顺序依赖有关。

1. React 依赖 Hooks 的调用顺序识别状态

React 内部通过 **“调用顺序”来跟踪每个 Hook 对应的状态。例如,当你在组件中多次调用 useStateuseEffect 时,React 会维护一个链表结构 **,按调用顺序存储每个 Hook 的状态信息(如 useState 的值、useEffect 的依赖等)。

每次组件渲染时,React 会按相同的顺序遍历这个链表,将 Hook 调用与对应的状态关联起来。如果 Hooks 的调用顺序发生变化,React 就会 “认错” 状态,导致状态混乱或报错。

2. 条件语句会破坏调用顺序的稳定性

即使条件 “一直为真”,将 Hook 放在条件语句中也会破坏 React 对 “调用顺序不变” 的假设

举个例子:

function MyComponent() {if (true) { // 即使条件永远为真const [count, setCount] = useState(0); // ❌ 错误:Hook 在条件中}const [name, setName] = useState('');// ...
}

表面上看,if (true) 似乎不会改变执行顺序,但 React 无法 “预知” 这个条件未来是否会变化。假设未来某天,这个条件被修改为 if (someFlag),而 someFlag 在某次渲染中变为 false,此时:

  • 第一次渲染:执行 useState(0) → 执行 useState('')(顺序:[0, 1])
  • 第二次渲染:不执行 useState(0) → 执行 useState('')(顺序:[1])

React 会发现第二次渲染的 Hook 数量 / 顺序与第一次不一致,直接抛出错误:Hooks can only be called inside the body of a function component

3. 规则的设计是为了 “防患于未然”

React 禁止在条件、循环、嵌套函数中调用 Hooks,本质上是一种强制约束,确保开发者写出的代码符合 “Hook 调用顺序稳定” 的要求。

即使你能保证当前条件 “一直为真”,但代码维护中可能会被修改(比如添加 else 分支、调整条件判断),此时隐藏的顺序问题就会暴露。React 通过提前禁止这种写法,避免了潜在的难以调试的状态错误。

总结

Hooks 的调用必须满足 “在组件顶层、每次渲染顺序一致”,这与 React 内部通过顺序跟踪状态的机制紧密相关。条件语句(即使条件恒真)会破坏这种稳定性,因此被 React 明确禁止。这一规则不是技术限制,而是为了保证组件状态管理的可靠性。

react fiber跟useEffect是怎么关联的

React Fiber 和 useEffect 看似是 React 中两个独立的概念(Fiber 是架构底层引擎,useEffect 是上层 Hooks API),但它们的关联体现在 Fiber 架构为 useEffect 的执行时机、调度优先级和副作用管理提供了底层支撑。具体关联可以从以下几个角度理解:

  1. Fiber 架构的两阶段工作模式决定了 useEffect 的执行时机

React Fiber 核心是将渲染工作拆分为「协调(Reconciliation)」和「提交(Commit)」两个阶段:

  • 协调阶段:找出前后 DOM 树的差异(Diff 算法),可被中断、暂停或恢复(为了高优先级任务让路,比如用户输入)。
  • 提交阶段:执行实际的 DOM 操作(插入 / 更新 / 删除节点),此阶段不可中断。

useEffect 的副作用执行时机,正依赖于 Fiber 的这两个阶段:

  • 协调阶段:React 会在 Fiber 节点上标记副作用(包括 useEffect 的回调和清理函数),并判断依赖数组是否变化,决定是否需要执行新的副作用或清理旧的副作用。
  • 提交阶段之后useEffect 的回调函数会在 DOM 更新完成(提交阶段结束)后异步执行。这是因为 Fiber 允许在提交阶段后,将副作用调度到浏览器空闲时执行,避免阻塞主线程。
  1. Fiber 的调度机制控制 useEffect 的优先级

Fiber 架构的核心能力是「优先级调度」—— 可以为不同任务分配优先级(如用户输入 > 渲染 > 副作用),高优先级任务可打断低优先级任务。

useEffect 的执行被归为「低优先级任务」,这正是由 Fiber 的调度机制(如 scheduler 包)实现的:

  • 当组件渲染完成(提交阶段结束)后,React 不会立即执行 useEffect 回调,而是通过 Fiber 调度器(如 requestIdleCallback 或模拟的空闲时间检测)将其推迟到浏览器主线程空闲时执行。
  • 这样做的好处是:避免副作用(如数据请求、日志打印)阻塞用户交互(如点击、输入)等高频优先级操作,保证应用响应性。
  1. Fiber 节点存储 useEffect 的副作用信息

在 Fiber 架构中,每个组件对应一个 Fiber 节点,节点上有一个 effectTag 属性和 effects 链表,用于存储副作用相关信息:

  • 当组件中使用 useEffect 时,React 会在协调阶段为对应的 Fiber 节点标记副作用类型(如 PassiveEffect,表示需要执行 useEffect)。
  • 所有标记了副作用的 Fiber 节点会被串联成 effects 链表,在提交阶段结束后,React 会遍历这个链表,执行 useEffect 的回调函数(并处理清理逻辑)。

简单说:Fiber 节点是 useEffect 副作用的「载体」,没有 Fiber 对副作用的存储和追踪,React 无法知道哪些组件需要执行副作用。

  1. useEffect 的清理函数与 Fiber 的中断恢复

当组件卸载或 useEffect 依赖变化时,需要执行清理函数(如取消订阅、清除定时器)。这一过程也依赖 Fiber 的特性:

  • 在 Fiber 协调阶段,如果发现组件需要卸载或 useEffect 依赖变化,会先标记「需要执行清理函数」,并将其加入副作用链表。
  • 由于 Fiber 支持任务中断,即使在协调阶段被高优先级任务打断,已标记的清理函数也会被妥善保存,待任务恢复后继续处理,确保清理逻辑不会丢失。

总结

React Fiber 是底层架构,为 useEffect 提供了:

  • 执行时机的保障(提交阶段后异步执行);
  • 优先级调度的能力(低优先级,不阻塞主线程);
  • 副作用的存储与追踪(通过 Fiber 节点的 effects 链表)。

可以说,useEffect 能以「不阻塞渲染、自动管理依赖和清理」的方式工作,其底层完全依赖于 Fiber 架构的设计。

useEffect 的执行时机可以简单概括为:
「渲染完成后执行,依赖控制执行频率,清理函数在卸载或重执行前触发」
它整合了类组件中 componentDidMountcomponentDidUpdatecomponentWillUnmount 三个生命周期的功能,通过依赖数组灵活控制副作用的执行时机。

useEffect 第二个参数如果不要会造成什么问题

在 React 中,useEffect 的第二个参数(依赖数组)用于控制副作用的执行时机。如果省略这个参数,会导致以下问题:

  1. 性能问题:副作用频繁执行

useEffect 的回调函数会在每次组件渲染完成后都执行(包括初始渲染和所有更新渲染)。

如果副作用包含耗时操作(如 API 请求、大量 DOM 操作、复杂计算等),会导致组件每次更新都重复执行这些操作,显著降低性能,甚至引发页面卡顿。

  1. 可能引发无限循环

如果副作用内部修改了组件状态(如 setState),而状态变化又会触发组件重新渲染,会形成「渲染 → 副作用执行 → 状态更新 → 再次渲染」的无限循环,最终导致应用崩溃。

例如:

function Counter() {const [count, setCount] = useState(0);// 省略第二个参数,每次渲染后都会执行useEffect(() => {// 修改状态 → 触发重新渲染 → 再次执行effect → ...无限循环setCount(prev => prev + 1);}); // 没有依赖数组return <div>{count}</div>;
}
  1. 资源管理问题

对于需要清理的副作用(如事件监听、定时器、订阅等),省略依赖数组会导致:

  • 每次渲染都重复创建资源(如重复绑定事件)
  • 清理函数(effect 回调返回的函数)会在每次渲染前执行,但可能无法彻底清理旧资源,最终导致内存泄漏。

例如:

useEffect(() => {// 每次渲染都会新增一个事件监听(旧的可能没被正确清理)window.addEventListener('resize', handleResize);return () => {window.removeEventListener('resize', handleResize);};
}); // 没有依赖数组 → 多次绑定/解绑,可能残留监听

正确做法

  • 空依赖数组 []:副作用仅在组件初始渲染后执行一次,类似类组件的 componentDidMount
  • 包含依赖项[dep1, dep2],副作用仅在依赖项发生变化时执行,避免不必要的重复执行。
// 仅初始渲染执行一次
useEffect(() => {fetchData();
}, []);// 仅当 userId 变化时执行
useEffect(() => {fetchUser(userId);
}, [userId]);

总之,省略 useEffect 的第二个参数会导致副作用过度执行,引发性能、逻辑甚至崩溃问题,应根据实际需求明确指定依赖项。

useEffect怎么渲染到浏览器

useEffect 本身并不直接参与「渲染到浏览器」的过程 ——React 中负责将组件渲染到浏览器 DOM 的是 React 的核心渲染机制(如协调阶段、提交阶段),而 useEffect 是在渲染完成后处理「副作用」的工具。它与浏览器渲染的关联在于:它总是在组件已经渲染到浏览器 DOM 之后执行,可以安全地与浏览器环境交互(如操作 DOM、添加事件监听等)。

具体流程:从状态更新到 useEffect 执行

  1. 状态 / Props 变化触发渲染
    当组件的 stateprops 变化时,React 会触发重新渲染:
    • 先执行组件函数,生成新的虚拟 DOM(VNode)。
    • 进入「协调阶段(Reconciliation)」:对比新旧虚拟 DOM,计算出需要更新的 DOM 节点(Diff 算法)。
  2. 提交阶段:更新浏览器 DOM
    协调完成后,React 进入「提交阶段(Commit)」:
    • 将计算出的 DOM 变化实际应用到浏览器 DOM 中(如插入、更新、删除节点)。
    • 此时,浏览器已经完成了 DOM 的更新(可以理解为「已经渲染到浏览器了」)。
  3. useEffect 执行:在渲染后处理副作用
    提交阶段结束后,React 会异步执行 useEffect 的回调函数:
    • 此时 DOM 已经是最新的,可以安全地读取或修改 DOM(比如获取元素尺寸、添加滚动监听等)。
    • 这一步是「非阻塞」的:useEffect 会被调度到浏览器主线程空闲时执行,不会阻塞用户交互或页面绘制。

示例:useEffect 在渲染后操作 DOM

比如在 useEffect 中获取渲染后的 DOM 元素属性:

import { useState, useEffect, useRef } from 'react';function Example() {const [count, setCount] = useState(0);const divRef = useRef(null);// 组件渲染到浏览器后执行useEffect(() => {// 此时 div 已经被渲染到 DOM 中,可以安全访问其属性if (divRef.current) {console.log('div 宽度:', divRef.current.offsetWidth);}}, [count]); // 依赖 count,count 变化后会重新执行return (<div ref={divRef} style={{ padding: count * 10 }}>点击次数: {count}<button onClick={() => setCount(c => c + 1)}>点击</button></div>);
}
  • 当点击按钮时,count 变化 → 组件重新渲染 → DOM 更新(div 的 padding 变化)→ useEffect 执行 → 读取到更新后的 div 宽度。

关键区别:useEffectuseLayoutEffect

如果需要在浏览器「绘制(paint)」之前执行副作用(比如同步修改 DOM 避免闪烁),可以用 useLayoutEffect,它的执行时机是:

  • 提交阶段 DOM 更新后 立即同步执行(在浏览器绘制前),会阻塞渲染。

useEffect异步执行(在浏览器绘制后),不会阻塞渲染,是大多数场景的首选。

总结

useEffect 不直接参与「渲染到浏览器」的过程,而是在渲染完成(DOM 已更新)后执行,专门用于处理需要与浏览器环境交互的副作用(如 DOM 操作、订阅、数据请求等)。它的执行时机确保了操作基于最新的 DOM 状态,且不会阻塞页面渲染。

React 中rende函数的原理是什么?

记忆点:类组件中表现为返回虚拟dom,描述UI结构(props跟state改变进行调用,纯函数)

React整体的渲染机制

1.生成虚拟dom=>2.协调器进行协调,对比旧的虚拟dom(diff算法)=>3.提交更新真实dom

在 React 中,“render 函数” 的概念可以从两个层面理解:类组件中的 render() 方法React 整体的渲染机制。前者是组件输出 UI 的入口,后者是 React 将组件描述转换为真实 DOM 的底层流程。

一、类组件中 render() 方法的作用

在类组件中,render() 是一个必须实现的核心方法,它的作用是:
根据组件当前的 propsstate,返回一个React 元素(虚拟 DOM),描述组件应该渲染的 UI 结构。

class MyComponent extends React.Component {render() {// 根据 props 和 state 返回 React 元素return <div>Hello, {this.props.name}</div>;}
}

核心特点

  1. 纯函数特性render() 本身不修改组件状态,也不产生副作用(如数据请求、DOM 操作),仅根据输入(this.propsthis.state)返回输出(React 元素)。
  2. 条件触发:当组件的 propsstate 发生变化时,React 会重新调用 render(),生成新的 React 元素,为后续的 “DOM 更新” 做准备。

二、React 渲染机制的底层原理

React 的整体渲染流程可以分为三个阶段render() 方法是触发这一流程的起点之一:

1. 生成虚拟 DOM(Virtual DOM)

  • 虚拟 DOM 是一个轻量级的 JavaScript 对象,结构与真实 DOM 一致(包含标签名、属性、子元素等),但不涉及浏览器渲染层的操作。

  • 类组件通过 render() 返回虚拟 DOM;函数组件则直接通过返回值生成虚拟 DOM(函数组件本身可视为一个 “render 函数”)。

    // 虚拟 DOM 的简化结构(实际由 React 内部创建)
    const virtualDOM = {type: 'div',props: {children: 'Hello, React'}
    };
    

2. 协调(Reconciliation):对比新旧虚拟 DOM(Diff 算法)

当组件的 propsstate 变化时,React 会生成新的虚拟 DOM,并与旧的虚拟 DOM 进行对比(这一过程称为 “协调”),找出两者的差异(“Diff”)。

React 的 Diff 算法有三个核心优化策略:

  • 同层比对:只对比同一层级的节点,不跨层级比较(减少复杂度)。

  • 类型判断:若节点类型(如 divspan)不同,直接销毁旧节点并创建新节点。

  • key 复用:列表节点通过 key 属性标识唯一性,相同 key 的节点会被优先复用(避免不必要的销毁 / 创建)。

    // 旧虚拟 DOM
    <ul><li key="1">Item 1</li><li key="2">Item 2</li>
    </ul>// 新虚拟 DOM(只更新 Item 2,复用 Item 1)
    <ul><li key="1">Item 1</li><li key="2">Item 2 Updated</li>
    </ul>
    

3. 提交(Commit):更新真实 DOM

协调阶段找出差异后,React 进入 “提交” 阶段,将差异批量更新到真实 DOM 中。

  • 对于新增的虚拟 DOM 节点:创建对应的真实 DOM 元素并插入页面。
  • 对于删除的虚拟 DOM 节点:移除对应的真实 DOM 元素。
  • 对于修改的虚拟 DOM 节点:仅更新变化的属性(如 classNamestyle 等),而非重新创建整个节点。

三、总结:render 函数在整体流程中的角色

  1. render() 是组件输出 UI 描述(虚拟 DOM)的入口,触发于 propsstate 变化时。
  2. React 通过对比新旧虚拟 DOM(Diff 算法),计算出最小更新范围,避免全量重渲染。
  3. 最终只将必要的变更应用到真实 DOM,实现 “高效更新”(这也是 React 性能优势的核心原因)。

简单来说:render() 负责 “描述 UI 应该是什么样”,而 React 底层机制负责 “高效地让真实 DOM 变成描述的样子”。

什么是Suspense组件?它解决了什么问题?

记忆点::主要用于协调异步操作的加载状态,让组件在等待异步数据(如网络请求、代码分割)时能够优雅地显示占位内容(loading 状态),而无需手动管理复杂的加载逻辑。将异步加载的 “等待逻辑” 从组件中抽离,通过声明式方式统一管理加载状态

向上查找,并发控制(暂停低任务优先渲染用户相关内容),批量处理,(进行合并),react18,增强持数据加载与服务器渲染

Suspense 是 React 16.6 引入的组件,主要用于协调异步操作的加载状态,让组件在等待异步数据(如网络请求、代码分割)时能够优雅地显示占位内容(loading 状态),而无需手动管理复杂的加载逻辑。

核心作用:解决异步加载的 “状态碎片化” 问题

Suspense 出现前,处理异步数据加载通常需要:

  1. 定义 loading 状态(如 const [loading, setLoading] = useState(true)
  2. 异步操作开始时设为 true,结束后设为 false
  3. 在组件中通过条件判断显示加载态或内容({loading ? <Spinner /> : <Content />}

这种方式的问题在于:

  • 加载状态与业务逻辑混杂,代码冗余
  • 多个异步操作时,需要管理多个 loading 状态,容易出错
  • 无法统一协调多个异步依赖的加载顺序

Suspense 的工作方式

Suspense 通过声明式语法,将 “等待异步资源” 与 “显示加载态” 分离,核心用法:

// 用 Suspense 包裹可能需要等待异步资源的组件
<Suspense fallback={<Spinner />}>{/* 子组件加载异步资源时,会触发 Suspense 显示 fallback */}<AsyncComponent />
</Suspense>
  • fallback:必填属性,指定异步资源加载完成前显示的占位内容(如加载动画)
  • Suspense 的子组件(或深层子组件)正在加载异步资源时,React 会暂停渲染,并显示 fallback 内容
  • 异步资源加载完成后,自动替换为实际内容

适用场景

  1. 代码分割(动态导入)
    配合 React.lazy 实现组件的按需加载,是 Suspense 最成熟的应用场景:

    import { Suspense, lazy } from 'react';// 动态导入组件(返回 Promise)
    const LazyComponent = lazy(() => import('./LazyComponent'));function App() {return (<Suspense fallback={<div>Loading...</div>}><LazyComponent /></Suspense>);
    }
    
  2. 数据请求(React 18+ 实验性支持)
    配合 use 钩子或数据获取库(如 React Query、SWR 的 Suspense 模式),直接在组件中声明式获取数据:

    // 数据获取函数(返回 Promise)
    async function fetchUser(id) {const res = await fetch(`/api/users/${id}`);return res.json();
    }// 组件中直接使用,无需手动管理 loading
    function UserProfile({ id }) {const user = fetchUser(id); // 触发 Suspensereturn <div>{user.name}</div>;
    }// 上层用 Suspense 统一管理加载态
    function App() {return (<Suspense fallback={<Spinner />}><UserProfile id="123" /></Suspense>);
    }
    

关键特性

  • 向上查找:如果子组件触发了 Suspense,React 会向上查找最近的 Suspense 组件并显示其 fallback
  • 并发控制:在 React 并发模式下,Suspense 可以暂停低优先级渲染,优先显示用户交互相关内容
  • 批量处理:多个异步依赖可以被同一个 Suspense 统一管理,只需一个 fallback

Suspense 增强:支持数据加载与服务器渲染

React 18 扩展了 Suspense 的能力,使其从 “仅支持代码分割” 升级为 “可协调任意异步操作”:

  • 数据加载:配合 use 钩子(React 18.3+)或数据库(如 React Query、SWR 的 Suspense 模式),Suspense 可直接等待数据请求完成,自动显示 fallback 加载态(无需手动管理 loading 状态)。
  • 服务器组件(Server Components):在服务器渲染中,Suspense 可将页面拆分为 “优先渲染” 和 “延迟渲染” 的部分,先发送已准备好的内容到客户端,提升首屏加载速度。
// React 18 中,Suspense 可直接等待数据加载
<Suspense fallback={<Spinner />}><UserProfile userId={1} /> {/* 组件内部获取数据,触发 Suspense */}
</Suspense>

总结

Suspense 解决的核心问题是:将异步加载的 “等待逻辑” 从组件中抽离,通过声明式方式统一管理加载状态,让开发者更专注于业务逻辑,同时简化代码结构、提升可维护性。目前在代码分割场景中已稳定可用,在数据请求场景中仍在持续优化(React 18+ 提供更好的支持)。

react中setState的执行机制和实现原理?

记忆点:执行机制:批处理优先,行为因场景而异,核心原则批量处理优先

实现原理:主要依赖于更新队列” 和 “调度系统”,入队更新:创建更新对象并加入队列=>调度更新=>合并计算=>触发渲染:更新dom

在 React 中,setState 是类组件更新状态的核心 API,其设计围绕 “高效更新” 和 “灵活控制” 展开。理解其执行机制和实现原理,需要从行为表现底层逻辑两个层面拆解:

一、执行机制:批处理为核心,行为因场景而异

setState 的核心特性是 “批处理更新”,但具体行为会因调用场景和 React 版本而有所不同,并非简单的 “同步” 或 “异步”。

1. 核心原则:批处理(Batching)优先

React 会将多个 setState 调用合并为一次更新,减少不必要的重渲染,这是性能优化的关键。

  • 批处理生效场景(表现为 “异步合并”):

    • React 合成事件回调(如 onClickonChange):React 会包裹事件处理逻辑,自动开启批处理。
    • 生命周期函数(如 componentDidMountcomponentDidUpdate):在组件生命周期流程中,批处理默认开启。
    • React 18+ 中的大部分场景(包括 setTimeoutPromise.then 等异步回调):通过 “自动批处理” 统一生效。
    // 示例:批处理合并更新
    handleClick = () => {this.setState({ count: 1 });this.setState({ name: 'Bob' });// 最终只触发一次重渲染,state 同时更新 count 和 name
    };
    

2. 非批处理场景(表现为 “同步更新”)

在少数场景下,批处理不会生效,setState 会立即更新状态并触发渲染:

  • React 17 及之前:原生 DOM 事件(addEventListener 绑定)、setTimeout/setIntervalPromise 回调等 “非 React 控制的上下文”。

  • 所有版本:通过 ReactDOM.flushSync 强制同步更新(主动打破批处理)。

    // React 17 中,setTimeout 回调内批处理不生效
    componentDidMount() {setTimeout(() => {this.setState({ count: 1 });console.log(this.state.count); // 输出 1(同步更新)}, 0);
    }// 强制同步更新(所有版本)
    import { flushSync } from 'react-dom';
    flushSync(() => {this.setState({ count: 1 });
    });
    console.log(this.state.count); // 输出 1(同步更新)
    

3. 函数式更新:依赖前序状态的正确姿势

当新状态依赖于上一次状态(如累加、切换)时,必须使用函数形式setState,否则可能因批处理合并导致计算错误。

// 错误:依赖未更新的 state,多次调用可能只生效一次
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 }); // 实际只加 1// 正确:函数形式接收前一次状态,确保计算准确
this.setState(prevState => ({ count: prevState.count + 1 }));
this.setState(prevState => ({ count: prevState.count + 1 })); // 最终加 2

二、实现原理:四步完成状态更新

setState 的底层逻辑依赖于 React 的 “更新队列” 和 “调度系统”,核心流程可分为四步:

1. 入队更新:创建更新对象并加入队列

调用 setState 时,React 不会立即修改 state,而是创建一个更新对象(包含新状态、更新类型等信息),并将其加入当前组件的更新队列updateQueue)。

  • 每个组件实例维护一个独立的更新队列,确保状态更新按调用顺序处理。
  • 多次调用 setState 会生成多个更新对象,依次入队等待处理。

2. 调度更新:由 Scheduler 决定处理时机

React 的调度器(Scheduler) 会根据当前上下文判断何时处理更新队列:

  • 若处于 “批处理模式”(如合成事件回调),调度器会延迟处理,等待当前事件循环中的所有 setState 调用完成后再统一处理。
  • 若处于 “非批处理模式”(如 setTimeout 回调),调度器会立即触发处理(React 17 及之前)。
  • React 18 后,调度器通过 “并发渲染” 架构,可中断、暂停或恢复更新,优先处理高优先级任务(如用户输入)。

3. 处理更新:合并更新并计算新状态

调度器触发处理后,React 会遍历组件的更新队列,通过 “合并函数” 计算最终状态:

  • 对于对象形式setState({ ... }):直接浅层合并(只合并顶层属性,嵌套对象需手动处理)。
  • 对于函数形式setState(prev => { ... }):按顺序执行函数,将前一次计算结果作为下一次的输入,确保依赖正确。

4. 触发渲染:更新 DOM

计算出新状态后,React 会触发组件的 “重渲染流程”:

  1. 调用 render 方法生成新的虚拟 DOM(Virtual DOM)。
  2. 通过 Diff 算法对比新旧虚拟 DOM,找出最小更新差异。
  3. 将差异批量更新到真实 DOM(提交阶段),完成视图更新。

三、关键结论

  1. setState 的核心是批处理机制,目的是减少重渲染次数,优化性能。
  2. 行为表现(同步 / 异步)取决于是否处于 “React 控制的批处理上下文”,React 18 通过自动批处理统一了大多数场景的行为。
  3. 依赖前序状态时必须使用函数形式,避免因批处理合并导致的计算错误。
  4. 底层通过 “更新队列”+“调度器” 实现,兼顾性能与灵活性。

理解这些机制,能帮助开发者避开 “状态更新延迟”“计算错误” 等常见陷阱,写出更可靠的代码。

setState是异步更新还是同步更新

记忆点:取决于 调用场景 和 React 的批量更新机制。

在 React 中,setState 的执行时机(异步或同步)取决于 调用场景React 的批量更新机制。理解这一点对于避免常见的状态更新陷阱非常重要。

一、核心结论

  1. 异步场景(默认行为):
    • 合成事件(React 事件系统中的事件,如 onClickonChange)。
    • 生命周期方法(如 componentDidMountrender)。
    • 批量更新:多个 setState 会被合并为一次更新。
  2. 同步场景
    • 原生事件(如 addEventListener 绑定的事件)。
    • 定时器(如 setTimeoutsetInterval)。
    • Promise 回调(如 .then())。
    • 手动调用 ReactDOM.flushSync()

二、异步场景详解

1. 合成事件中的异步行为

class Counter extends React.Component {state = { count: 0 };handleClick = () => {this.setState({ count: this.state.count + 1 });console.log(this.state.count); // 输出 0(异步更新,尚未生效)};render() {return <button onClick={this.handleClick}>{this.state.count}</button>;}
}

原因:React 会在事件处理函数执行完毕后 批量更新状态,以提升性能。

2. 生命周期方法中的异步行为

componentDidMount() {this.setState({ data: 'loaded' });console.log(this.state.data); // 输出 undefined(异步更新,尚未生效)
}

三、同步场景详解

1. 原生事件中的同步行为

componentDidMount() {document.getElementById('btn').addEventListener('click', () => {this.setState({ count: 1 });console.log(this.state.count); // 输出 1(同步更新)});
}

原因:原生事件不受 React 事件系统控制,状态更新会立即执行。

2. 定时器中的同步行为

setTimeout(() => {this.setState({ count: 1 });console.log(this.state.count); // 输出 1(同步更新)
}, 0);

3. Promise 回调中的同步行为

fetchData().then(() => {this.setState({ data: 'loaded' });console.log(this.state.data); // 输出 'loaded'(同步更新)
});

四、批量更新机制

React 会将 同一事件循环内 的多个 setState 合并为一次更新,以减少渲染次数:

handleClick = () => {this.setState({ count: this.state.count + 1 }); // 第一次调用this.setState({ count: this.state.count + 1 }); // 第二次调用// 最终 count 只增加 1,而非 2!
};

解决方案:使用函数式 setState 确保每次更新基于最新状态:

handleClick = () => {this.setState(prevState => ({ count: prevState.count + 1 })); // 第一次调用this.setState(prevState => ({ count: prevState.count + 1 })); // 第二次调用// 最终 count 增加 2
};

五、强制同步更新(React 16+)

使用 ReactDOM.flushSync() 强制同步执行状态更新:

import ReactDOM from 'react-dom';handleClick = () => {ReactDOM.flushSync(() => {this.setState({ count: this.state.count + 1 });});console.log(this.state.count); // 输出 1(同步更新)
};

注意:过度使用 flushSync 会影响性能,应谨慎使用。

六、总结与最佳实践

  1. 默认假设 setState 是异步的,避免依赖更新后的状态立即执行代码。

  2. 使用回调函数获取最新状态

    this.setState({ count: 1 }, () => {console.log(this.state.count); // 输出 1(回调在更新后执行)
    });
    
  3. 优先使用函数式 setState

    处理依赖前一个状态的更新:

    this.setState(prevState => ({ count: prevState.count + 1 }));
    
  4. 理解批量更新规则:同一事件循环内的多个 setState 会被合并。

通过合理处理 setState 的异步特性,可以避免常见的状态管理陷阱,写出更可靠的 React 应用。

setState 的 “异步 / 同步” 差异,本质上是 React 在 性能优化(批量更新)场景可控性 之间的权衡:

  • 对于 React 可控的场景(合成事件、生命周期),通过异步批量更新减少 DOM 操作,提升性能;
  • 对于 React 不可控的场景(原生事件、定时器等),通过同步更新保证状态与 DOM 的一致性,避免不可预期的行为。

理解这一点,就能从底层逻辑上解释为什么 setState 会有看似矛盾的行为 —— 它不是 “随机的异步或同步”,而是 React 为了兼顾性能和可靠性设计的合理机制。

在 React 中,子组件因useEffect导致重复渲染和重复请求,通常与依赖项处理、组件记忆化或数据缓存有关。以下是具体解决方法:

1. 优化useEffect的依赖项

useEffect会在依赖项变化时重新执行。若依赖项是每次渲染都会生成新引用的值(如对象、数组、匿名函数),会导致不必要的重复执行。

错误示例

// 子组件
function Child({ id }) {// 每次渲染都会创建新对象,导致useEffect重复执行const params = { id: id }; useEffect(() => {fetchData(params); // 重复请求}, [params]); // 错误:params引用每次都变
}

解决方法

  • 依赖项使用原始值(而非对象 / 数组)
  • useMemo记忆复杂依赖项
function Child({ id }) {// 用useMemo记忆对象,确保引用稳定const params = useMemo(() => ({ id }), [id]); useEffect(() => {fetchData(params); }, [params]); // 仅当id变化时执行
}
  1. 防止子组件不必要的重渲染

父组件重渲染时,子组件可能被连带重渲染,导致useEffect重复触发。可通过以下方式优化:

(1)用React.memo记忆子组件

React.memo会浅比较 props,若 props 未变化则阻止子组件重渲染。

// 用React.memo包装子组件
const Child = React.memo(({ id, fetchData }) => {useEffect(() => {fetchData(id);}, [id, fetchData]);return <div>...</div>;
});

(2)用useCallback记忆父组件传递的函数

父组件的函数若未被记忆,每次渲染会生成新引用,导致子组件的fetchData props 变化,触发重渲染。

// 父组件
function Parent() {// 用useCallback记忆函数,确保引用稳定const fetchData = useCallback((id) => {// 请求逻辑}, []); // 依赖项为空,函数仅创建一次return <Child id={id} fetchData={fetchData} />;
}

3. 缓存请求结果,避免重复请求

即使useEffect执行,也可通过缓存避免重复请求相同数据(如用useState+ 条件判断,或专门的缓存库)。

示例:本地缓存请求结果

function Child({ id }) {const [data, setData] = useState(null);const [loading, setLoading] = useState(false);useEffect(() => {// 若已有数据,直接返回(缓存生效)if (data?.id === id) return;const fetchData = async () => {setLoading(true);try {const res = await api.get(`/data/${id}`);setData(res.data);} catch (err) {console.error(err);} finally {setLoading(false);}};fetchData();}, [id, data]); // 依赖id和当前数据return <div>{data?.content}</div>;
}

4. 清理未完成的请求

组件卸载或依赖项变化时,若前一次请求未完成,可能导致内存泄漏或错误。需在useEffect的清理函数中取消请求。

示例:用 AbortController 取消请求

useEffect(() => {const controller = new AbortController(); // 创建控制器const signal = controller.signal;const fetchData = async () => {try {const res = await fetch(`/data/${id}`, { signal }); // 关联信号const data = await res.json();setData(data);} catch (err) {if (err.name !== 'AbortError') { // 忽略主动取消的错误console.error(err);}}};fetchData();// 清理函数:取消未完成的请求return () => controller.abort();
}, [id]);

总结

解决步骤优先级:

  1. 检查并优化useEffect的依赖项(核心原因)
  2. React.memo+useCallback防止子组件无效重渲染
  3. 缓存请求结果(减少重复请求)
  4. 清理未完成的请求(避免副作用残留)

通过以上方法,可有效避免子组件因useEffect导致的重复渲染和请求问题。

useState,每一次函数重新渲染的时候,useState怎么能够做到返回的状态是之前的状态呢?

在 React 中,useState 之所以能在函数组件重新渲染时返回 “之前的状态”,核心在于 React 内部为每个组件实例维护了一份独立的 “状态存储”,且通过 “调用顺序” 与 useState 建立了稳定的关联。

具体原理拆解:

  1. 状态不存储在函数作用域中

函数组件每次重新渲染时,函数体都会被重新执行。如果状态像普通变量一样存储在函数作用域中(例如 let count = 0),那么每次执行函数都会被重置为初始值。

useState 的状态 存储在 React 内部为组件实例分配的 “内存空间” 中(可以理解为一个与组件实例绑定的对象 / 链表),与函数的执行上下文无关。因此,无论函数渲染多少次,状态都会被 “持久化” 保存。

  1. 通过 “调用顺序” 关联状态

React 会为每个组件实例维护一个 “Hooks 链表”,链表中的每个节点对应一个 Hook(如 useStateuseEffect 等)的信息(包括状态值、更新函数等)。

当组件第一次渲染时:

  • 执行 const [count, setCount] = useState(0),React 会在链表中创建一个新节点,存储初始状态 0,并返回 [状态值, 更新函数]

当组件重新渲染时:

  • React 会按照 “Hooks 的调用顺序” 遍历链表,找到对应 useState 节点中存储的 “最新状态”,并返回该状态。

这也是为什么 Hooks 必须在函数组件顶层调用,不能放在条件 / 循环中—— 如果调用顺序改变,React 会无法匹配到正确的状态节点,导致状态错乱。

  1. 更新状态时的 “重新渲染触发”

当调用 setCount(newValue) 时,React 会:

  1. 更新该 useState 节点中存储的状态值(替换为 newValue)。
  2. 标记组件需要重新渲染。
  3. 组件重新执行时,useState 会从链表中读取更新后的状态值并返回。

举个简化的类比:

可以把组件实例想象成一个 “文件夹”,useState 的状态就是文件夹里的 “文件”。

  • 第一次渲染:创建文件夹,新增一个文件(比如 count.txt),内容是初始值 0
  • 重新渲染:打开同一个文件夹,读取 count.txt 的最新内容(无论文件夹被打开多少次,文件内容都由上次保存的结果决定)。
  • 调用 setCount:修改 count.txt 的内容,并通知 “系统” 重新打开文件夹查看。

关键结论:

useState 能在重新渲染时返回之前的状态,本质是因为:

  1. 状态存储在 React 内部与组件实例绑定的独立空间中(而非函数作用域)。
  2. 通过固定的调用顺序,useState 每次都能准确找到对应的状态节点。

这种设计既保证了函数组件的简洁性,又实现了状态的持久化管理。

什么是react插槽(Portals)?说明一下它的使用场景?

记忆点:逻辑与DOM分离

  • 从 React 组件树的角度看,通过 Portals 渲染的内容仍然是当前组件的子元素,会正常参与组件的生命周期、事件冒泡等逻辑。
  • 从 DOM 结构的角度看,这部分内容会被插入到指定的目标 DOM 节点中,脱离原有的父组件 DOM 层级。

悬浮框,提示符,通知提示

在 React 中,Portals( portals,直译为 “门户”) 是一种特殊的渲染机制,允许你将组件的子元素渲染到父组件的 DOM 层级结构之外的另一个 DOM 节点中。简单来说,就是组件的逻辑上的父级仍然是 React 组件树中的父组件,但视觉上的渲染位置可以 “跳出” 原有的 DOM 层级,插入到页面的其他地方(如 body 标签下)。

一、Portals 的基本用法与原理

Portals 通过 ReactDOM.createPortal() 方法实现,语法如下:

import ReactDOM from 'react-dom';function MyComponent() {// 第一个参数:要渲染的内容(React 元素、组件等)// 第二个参数:目标 DOM 节点(必须是真实的 DOM 元素)return ReactDOM.createPortal(<div>这是通过 Portals 渲染的内容</div>,document.getElementById('portal-container') // 目标容器);
}

核心原理

  • 从 React 组件树的角度看,通过 Portals 渲染的内容仍然是当前组件的子元素,会正常参与组件的生命周期、事件冒泡等逻辑。
  • 从 DOM 结构的角度看,这部分内容会被插入到指定的目标 DOM 节点中,脱离原有的父组件 DOM 层级。

二、Portals 的关键特性

  1. 逻辑与 DOM 分离
    内容在 React 组件树中仍属于原组件的子节点(可访问父组件的 propsstate),但 DOM 位置独立,解决了 “逻辑归属” 与 “视觉位置” 不一致的问题。

  2. 事件冒泡正常生效
    尽管内容渲染在其他 DOM 节点,但事件(如 onClick)会正常冒泡到 React 组件树中的父组件。例如:

    function Parent() {const [show, setShow] = useState(false);return (<div onClick={() => setShow(false)}> {/* 点击父组件关闭弹窗 */}<button onClick={() => setShow(true)}>打开弹窗</button>{show && ReactDOM.createPortal(<div className="modal">弹窗内容</div>,document.body)}</div>);
    }
    

    点击弹窗内容时,事件会冒泡到 Parent 组件的外层 div,触发关闭逻辑(符合 React 事件冒泡机制)。

三、Portals 的典型使用场景

Portals 主要用于解决 “内容需要视觉上脱离父组件 DOM 层级” 的场景,尤其是当父组件存在样式限制(如 overflow: hiddenz-index 层级低)时,避免内容被截断或遮挡。

1. 模态框(Modal)

这是 Portals 最常见的场景。模态框通常需要覆盖整个页面,但如果父组件有 overflow: hidden 或固定高度,直接在父组件内渲染会导致模态框被截断。通过 Portals 将模态框渲染到 body 下,可避免此问题:

function Modal({ children, onClose }) {// 渲染到 body 下的独立容器,避免父组件样式影响return ReactDOM.createPortal(<div className="modal-overlay" onClick={onClose}><div className="modal-content" onClick={e => e.stopPropagation()}>{children}</div></div>,document.body);
}

2. 悬浮提示(Tooltip)或下拉菜单(Dropdown)

当组件(如下拉菜单)需要显示在父组件外部(如超出父容器边界)时,父组件的 overflow: hidden 会导致内容被隐藏。使用 Portals 可将其渲染到 body 下,确保完整显示:

function Dropdown({ options }) {return ReactDOM.createPortal(<div className="dropdown">{options.map(option => (<div key={option.id}>{option.label}</div>))}</div>,document.getElementById('dropdown-container'));
}

3. 通知提示(Notification/Toast)

全局通知(如操作成功提示、错误警告)通常需要显示在页面顶层,且不依赖于某个具体父组件。通过 Portals 将通知容器固定在 body 下,可实现全局显示:

function Notification({ message }) {return ReactDOM.createPortal(<div className="notification">{message}</div>,document.getElementById('notification-container'));
}

4. 富文本编辑器或弹窗组件

某些复杂组件(如富文本编辑器的弹窗工具栏、图片预览弹窗)需要突破编辑器容器的样式限制(如 z-index 较低、存在定位上下文),通过 Portals 可确保其显示在正确层级。

四、使用 Portals 的注意事项

  1. 目标 DOM 节点需提前存在
    确保 ReactDOM.createPortal() 的第二个参数(目标容器)在渲染时已存在于 DOM 中(可在 index.html 中提前定义,如 <div id="portal-container"></div>)。
  2. 样式隔离
    由于内容渲染在独立 DOM 节点,需注意样式冲突(可使用 CSS Modules 或命名空间避免)。
  3. 事件冒泡需谨慎处理
    虽然事件会正常冒泡,但如果不希望父组件响应事件(如点击模态框内容不触发背景关闭),需用 e.stopPropagation() 阻止冒泡。

总结

React Portals 是一种 “打破 DOM 层级限制” 的渲染机制,核心价值是让组件的逻辑归属与视觉渲染位置分离。其典型使用场景包括模态框、悬浮提示、全局通知等需要脱离父组件 DOM 层级的元素,解决了样式限制导致的内容截断、层级冲突等问题,同时保持了 React 组件模型的事件冒泡和生命周期特性。

react 组件的更新机制是怎样的?

记忆点:

用于在状态(state)或属性(props)变化时,高效更新 UI。这一机制围绕 “最小化 DOM 操作” 和 “优化渲染性能” 设计,涉及更新触发、任务调度、协调(Diff)、提交(DOM 更新) 等多个阶段,并依赖 Fiber 架构实现可中断的高效更新。

React 组件的更新机制是其核心功能之一,用于在状态(state)或属性(props)变化时,高效更新 UI。这一机制围绕 “最小化 DOM 操作” 和 “优化渲染性能” 设计,涉及更新触发、任务调度、协调(Diff)、提交(DOM 更新) 等多个阶段,并依赖 Fiber 架构实现可中断的高效更新。

一、更新的触发:什么会导致组件更新?

组件更新的源头是状态或属性的变化,具体触发场景包括:

  1. 状态变化(state
    • 类组件调用 this.setState()this.forceUpdate()
    • 函数组件调用 useState 的更新函数(如 setCount)或 useReducerdispatch 函数。
      状态变化会直接触发组件重新渲染。
  2. 属性变化(props
    • 父组件更新后,子组件接收的 props 发生变化,会触发子组件重新渲染(除非被优化手段阻止)。
  3. 上下文(Context)变化
    • 若组件通过 useContextContext.Consumer 消费上下文,当上下文 value 变化时,组件会重新渲染。
  4. 强制更新
    • 类组件调用 this.forceUpdate(),或函数组件中使用 useImperativeHandle 等强制触发更新(跳过状态检查)。

二、更新的核心流程:从触发到 DOM 更新

React 组件的更新过程可分为 4 个核心阶段,依赖 Fiber 架构实现高效调度和中断恢复:

阶段 1:更新入队与调度(Scheduling)

状态或属性变化后,React 不会立即执行更新,而是先将更新任务加入队列,并由 Scheduler(调度器) 决定何时执行。

  • 批处理(Batching):React 会将多个连续的更新(如同一事件回调中多次调用 setState)合并为一次更新,减少渲染次数。例如:

    // 类组件
    handleClick() {this.setState({ count: 1 });this.setState({ count: 2 }); // 两次更新会合并,最终 count 为 2
    }// 函数组件
    const handleClick = () => {setCount(c => c + 1);setCount(c => c + 1); // 合并为一次更新,count 最终 +2
    };
    

    (React 18 后,批处理在更多场景下生效,包括异步操作如 setTimeout 中。)

  • 优先级排序:Scheduler 根据任务优先级(如用户输入 > 动画 > 普通更新)决定执行顺序,高优先级任务(如点击、滚动)可中断低优先级任务(如列表渲染),避免卡顿。

阶段 2:协调(Reconciliation)—— 计算差异(可中断)

协调阶段是 React 计算 “需要更新哪些部分” 的核心阶段,依赖 Fiber 架构Diff 算法,且可被高优先级任务中断

  1. 构建新 Fiber 树
    从根组件开始,基于新的 state/props 重新生成组件的虚拟 DOM(通过 render 方法或函数组件执行),并映射为新的 Fiber 树(Fiber 是工作单元的载体)。
  2. Diff 算法对比新旧 Fiber 树
    React 通过 Diff 算法对比新旧 Fiber 树,计算最小差异:
    • 同层节点仅对比同层级(不跨层级),降低复杂度;
    • 节点类型不同则直接替换子树;
    • 节点类型相同则对比 props 和子节点,标记差异类型(如 “更新”“新增”“删除”)。
  3. 标记副作用(Effect Tag)
    对需要更新的节点标记副作用(如 UpdatePlacementDeletion),并收集到 “Effect List” 中,供后续提交阶段使用。

阶段 3:提交(Commit)—— 执行 DOM 更新(不可中断)

协调阶段完成后,React 进入提交阶段,根据 Effect List 执行实际的 DOM 操作,此阶段不可中断(避免 DOM 状态不一致)。

  1. 执行前置副作用(before mutation
    调用 getSnapshotBeforeUpdate(类组件)等生命周期钩子,获取更新前的 DOM 状态。
  2. 执行 DOM 操作
    根据 Effect List 中的标记,执行具体的 DOM 操作:
    • 新增节点(Placement):将新节点插入 DOM;
    • 更新节点(Update):更新 DOM 属性或内容(如 classNametextContent);
    • 删除节点(Deletion):移除旧节点,解绑事件。
  3. 执行后置副作用(layout
    • 调用类组件生命周期钩子(componentDidMountcomponentDidUpdate);
    • 调用函数组件的 useEffect 清理函数(上一次渲染的副作用)和回调函数;
    • 更新 ref 引用,使其指向最新的 DOM 节点。

阶段 4:清理与收尾

提交阶段后,React 会清理临时变量(如旧 Fiber 树),并通知 Scheduler 调度下一个等待的任务。

三、更新优化:避免不必要的重渲染

默认情况下,父组件更新会触发所有子组件重新渲染,即使子组件 props 未变化。React 提供了多种优化手段减少无效更新:

  1. React.memo(函数组件)
    对函数组件进行记忆化处理,仅当 props 真正变化时才重新渲染(浅比较 props)。

    const MemoizedChild = React.memo(ChildComponent);
    // 仅当 ChildComponent 的 props 浅比较不同时,才会重新渲染
    
  2. shouldComponentUpdate(类组件)
    类组件可重写此方法,通过自定义逻辑判断是否需要更新(返回 false 则跳过更新)。

    class ChildComponent extends React.Component {shouldComponentUpdate(nextProps) {// 仅当 props.id 变化时才更新return nextProps.id !== this.props.id;}
    }
    
  3. useMemouseCallback(函数组件)

    • useMemo 记忆化计算结果,避免每次渲染重复计算;
    • useCallback 记忆化函数引用,避免因函数重新创建导致子组件 props 变化。
    const Child = React.memo(({ onClick }) => <button onClick={onClick} />);const Parent = () => {// 记忆化 onClick 函数,避免每次渲染创建新函数const handleClick = useCallback(() => {}, []);return <Child onClick={handleClick} />;
    };
    
  4. 拆分组件与状态下沉
    将不相关的状态拆分到独立组件中,避免状态变化导致大面积重渲染。

四、函数组件与类组件更新的差异

虽然核心流程一致,但函数组件和类组件的更新在细节上存在差异:

  • 状态存储:类组件的状态存储在实例的 this.state 中;函数组件的状态通过 Hooks 存储在 Fiber 节点的链表中(依赖调用顺序)。
  • 更新触发:类组件依赖 setState 合并状态;函数组件的 useState 更新函数是独立的,不会合并状态(需通过函数式更新 setX(x => x + 1) 依赖前值)。
  • 生命周期:类组件通过生命周期钩子控制更新逻辑;函数组件通过 useEffectuseLayoutEffect 等 Hooks 处理副作用。

总结

React 组件的更新机制是一个 “触发→调度→协调→提交” 的完整流程,核心特点包括:

  1. 批处理与优先级调度:合并更新、优先处理高优先级任务,提升性能;
  2. Fiber 架构支持中断:协调阶段可被中断,避免主线程阻塞;
  3. Diff 算法精准更新:最小化 DOM 操作,只更新变化的部分;
  4. 丰富的优化手段:通过 React.memouseMemo 等减少无效重渲染。

这一机制既保证了 UI 与状态的一致性,又通过多种优化策略确保了复杂应用的流畅性。

react的Immutable的原理是什么?

记忆点:

问题,解决状态难以追踪,导致无效的重渲染或漏渲染,“数据一旦创建就不可修改,任何修改都会返回一个新的数据源”。

核心:不可变性,结构共享性。

在 React 中,Immutable(不可变数据) 并非 React 内置的功能,而是一种数据管理思想,其核心是 “数据一旦创建就不可修改,任何修改都会返回一个新的数据源”。这种思想在 React 中被广泛应用于状态(state)和属性(props)的管理,目的是提高状态对比的效率减少不必要的重渲染,并让状态变化更可预测。

一、为什么需要 Immutable?

JavaScript 中的对象和数组默认是可变的(Mutable):当你修改一个对象的属性或数组的元素时,会直接改变原对象(因为对象 / 数组是引用类型,变量存储的是内存地址)。这种特性在 React 中会带来问题:

  1. 状态变化难以追踪:直接修改原状态后,无法通过引用对比判断数据是否变化(原引用没变,但内容变了)。
  2. 导致无效重渲染或漏渲染:React 依赖 “引用对比” 判断是否需要更新组件(如 React.memoshouldComponentUpdate)。若原对象被修改但引用不变,React 会认为数据没变化,导致漏渲染;若频繁创建新对象(即使内容没变),则会触发无效重渲染。

二、Immutable 的核心原理

Immutable 的核心是 “不可变性”“结构共享”

1. 不可变性:修改数据时返回新对象,原对象保持不变

对于 Immutable 数据,任何修改操作(如添加、删除、更新属性)都不会改变原对象,而是返回一个全新的对象。原对象始终保持创建时的状态,这确保了数据的 “可追溯性”。

示例(原生 JavaScript 模拟 Immutable)

// 原对象(Immutable)
const original = { name: "React", version: 18 };// 修改时返回新对象,原对象不变
const updated = { ...original, version: 19 };console.log(original.version); // 18(原对象未变)
console.log(updated.version);  // 19(新对象)
console.log(original === updated); // false(引用不同,便于判断变化)

2. 结构共享:只复制变化的部分,复用未变化的部分

对于嵌套结构(如 { a: { b: { c: 1 } }, d: 2 }),如果每次修改都完整复制整个对象,会产生巨大的性能和内存开销。Immutable 通过 “结构共享” 优化:

  • 只复制被修改的属性及其所在的 “路径”;
  • 未被修改的属性复用原对象的引用,不重复占用内存。

示例(嵌套对象的结构共享)

// 原对象(嵌套结构)
const original = {user: { name: "Alice", age: 20 },settings: { theme: "light" }
};// 修改 user.age,返回新对象(结构共享)
const updated = {...original, // 复用原对象的引用(但实际是新对象)user: {      // user 被修改,创建新对象...original.user, // 复用 user 中未变的属性(name)age: 21           // 只修改 age}
};// 未修改的部分复用原引用
console.log(original.settings === updated.settings); // true(共享引用)
// 修改的部分是新引用
console.log(original.user === updated.user); // false(新对象)
console.log(original === updated); // false(新对象)

通过结构共享,既保证了不可变性,又避免了全量复制的性能损耗。

三、Immutable 在 React 中的应用价值

React 的更新机制严重依赖 “数据是否变化” 的判断,而 Immutable 数据通过 “引用对比” 就能高效判断变化,无需深比较,从而优化性能:

1. 优化 shouldComponentUpdate(类组件)

类组件中,shouldComponentUpdate 通过对比 nextPropsthis.propsnextStatethis.state 决定是否重渲染。若使用 Immutable 数据,只需浅比较引用即可:

class MyComponent extends React.Component {shouldComponentUpdate(nextProps, nextState) {// 浅比较:若引用不同,说明数据变化,需要重渲染return nextProps.data !== this.props.data || nextState.count !== this.state.count;}
}

如果数据是可变的(如直接修改原对象),nextProps.datathis.props.data 引用相同,即使内容变化,shouldComponentUpdate 也会返回 false,导致漏渲染。

2. 优化 React.memo(函数组件)

React.memo 对函数组件进行记忆化,默认浅比较 props。若 props 是 Immutable 数据,引用变化即代表内容变化,能精准触发重渲染:

// 记忆化组件:仅当 props.data 引用变化时才重渲染
const MemoizedComponent = React.memo(({ data }) => {return <div>{data.name}</div>;
});// 使用时:修改 data 会返回新引用,触发更新
const Parent = () => {const [data, setData] = useState({ name: "React" });const handleClick = () => {// 正确:返回新对象(Immutable 思想)setData(prev => ({ ...prev, name: "React 18" }));};return <MemoizedComponent data={data} />;
};

3. 让状态变化可预测,便于调试

Immutable 数据的 “不可修改” 特性确保了:

  • 任何时间点的状态都是 “快照”,不会被后续操作篡改;
  • 配合 Redux 等状态管理库时,可实现 “时间旅行”(回溯历史状态),便于调试。

四、实现 Immutable 的工具库

手动实现 Immutable 数据(尤其是嵌套结构)容易出错且繁琐,实际开发中常用工具库:

  1. Immer:最常用的库,通过 “draft 模式” 允许 “看似修改” 数据,实际自动生成 Immutable 新对象,语法简洁:

    import { produce } from "immer";const original = { count: 1 };
    // "修改" draft,实际生成新对象
    const updated = produce(original, draft => {draft.count += 1; // 看似直接修改,实际操作的是临时草稿
    });console.log(original.count); // 1(原对象不变)
    console.log(updated.count);  // 2(新对象)
    
  2. Immutable.js:提供完整的 Immutable 数据结构(如 MapList),但语法较特殊,需要学习新 API。

  3. 原生扩展:ES6+ 的扩展运算符(...)、Object.assign、数组方法(mapfilter 等,返回新数组)可实现简单的 Immutable 操作,但嵌套结构需要手动处理。

总结

React 中 Immutable 的核心原理是:数据一旦创建就不可修改,修改时通过 “结构共享” 返回新对象,仅复制变化部分,复用未变化部分。这种思想解决了 JavaScript 引用类型的 “可变” 特性带来的状态追踪难题,让 React 能通过高效的 “引用对比” 判断数据变化,减少无效重渲染,同时使状态变化更可预测、更易于调试。

在实际开发中,结合 Immer 等工具库,可轻松实现 Immutable 数据管理,显著提升 React 应用的性能和可维护性。

如何获取setState的最新值

记忆点:使用函数式更新,避免直接使用this.state

在 React 中,setState 的异步特性是为了优化性能(例如批量更新),但这可能导致难以立即获取最新状态值。以下是确保获取最新状态值的几种方法:

  1. 使用函数式更新(最可靠方式)

当新状态依赖于旧状态时,使用 函数式更新 确保每次更新都基于最新状态:

// 错误方式:可能使用旧的 this.state.count
this.setState({ count: this.state.count + 1 });// 正确方式:使用函数式更新,参数 prevState 是最新状态
this.setState(prevState => ({count: prevState.count + 1
}));

原理:React 会将所有函数式更新放入队列,按顺序执行,确保每个更新都基于上一次的结果。

  1. 在回调函数中获取最新值

setState 的第二个参数是回调函数,会在状态更新完成后执行

this.setState({ count: this.state.count + 1 },() => {console.log('最新值:', this.state.count); // 此时状态已更新}
);
  1. componentDidUpdate 中处理更新后逻辑

组件更新完成后,componentDidUpdate 会被调用,此时 this.state 是最新值:

componentDidUpdate(prevProps, prevState) {if (prevState.count !== this.state.count) {console.log('状态已更新为:', this.state.count);}
}
  1. 使用 useState 的函数式更新(Hooks 方式)

在函数组件中,使用 useState 的函数式更新和 useEffect 监听变化:

const [count, setCount] = useState(0);// 函数式更新,确保基于最新值
const increment = () => {setCount(prevCount => prevCount + 1);
};// 监听状态变化,更新后执行
useEffect(() => {console.log('最新 count:', count);
}, [count]);
  1. 避免直接依赖 this.state

不要在 setState 前后直接读取 this.state,而是通过上述方法间接获取最新值。例如:

// 错误:可能读取到旧值
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 可能不是最新值// 正确:使用回调或函数式更新
this.setState(prev => ({ count: prev.count + 1 }));

为什么需要这些方法?

React 可能会将多次 setState 合并为一次更新(例如在同一个事件处理函数中),以提高性能。如果直接依赖 this.state,可能会使用到未更新的值。而 函数式更新回调函数 是 React 提供的明确获取最新状态的方式。

总结

  • 函数式更新:适用于新状态依赖旧状态的场景(如计数、数组操作)。
  • 回调函数:适用于需要立即使用最新状态的场景(如更新 DOM)。
  • 生命周期 / 副作用:适用于在状态更新后执行额外逻辑(如网络请求)。

通过这些方法,可以确保在 React 中可靠地处理状态更新。

React中组件之间如何通信

在 React 中,组件通信的方式取决于组件之间的关系(父子、兄弟、跨层级、无关联等),不同场景需要选择不同的方案。以下是常见的组件通信方式及适用场景:

一、父子组件通信:最基础的通信方式

父子组件是最直接的关系,通信通过 props回调函数 实现,是 React 中最常用的通信模式。

1. 父组件 → 子组件:通过 props 传递数据

父组件将数据作为 props 传递给子组件,子组件通过 props 接收并使用。
示例

// 父组件
function Parent() {const parentData = "来自父组件的数据";return <Child message={parentData} />; // 通过 props 传递
}// 子组件
function Child(props) {return <div>子组件接收:{props.message}</div>; // 通过 props 接收
}

props 可以传递任意类型数据(基本类型、对象、函数、组件等),且是单向只读的(子组件不能直接修改 props,需通过父组件更新)。

2. 子组件 → 父组件:通过回调函数传递数据

父组件将一个回调函数作为 props 传给子组件,子组件调用该函数并传入数据,实现向父组件 “发送消息”。
示例

// 父组件
function Parent() {const [childData, setChildData] = useState("");// 定义回调函数,接收子组件的数据const handleChildMsg = (data) => {setChildData(data);};return (<div><Child onSendMsg={handleChildMsg} /><p>父组件接收:{childData}</p></div>);
}// 子组件
function Child(props) {const sendData = () => {// 调用父组件传入的回调函数,传递数据props.onSendMsg("来自子组件的数据");};return <button onClick={sendData}>发送给父组件</button>;
}

二、兄弟组件通信:通过父组件中转

兄弟组件(同一父组件的子组件)之间无法直接通信,需以父组件为中间层

  1. 子组件 A 将数据通过回调传给父组件;
  2. 父组件将数据通过 props 传给子组件 B。

示例

// 父组件(中间层)
function Parent() {const [sharedData, setSharedData] = useState("");return (<div><BrotherA onUpdate={setSharedData} /> {/* A 传数据给父组件 */}<BrotherB data={sharedData} /> {/* 父组件传数据给 B */}</div>);
}// 兄弟组件 A
function BrotherA(props) {const handleClick = () => {props.onUpdate("A 发送的数据"); // 传给父组件};return <button onClick={handleClick}>A 发送</button>;
}// 兄弟组件 B
function BrotherB(props) {return <div>B 接收:{props.data}</div>; // 从父组件接收
}

三、跨层级组件通信:Context API

当组件层级较深(如祖父→孙子→曾孙),通过 props 一层层传递(“props drilling”)会非常繁琐。此时可使用 Context API 创建全局上下文,让所有后代组件直接访问数据。

Context 通信流程:

  1. 创建 Context:使用 createContext 创建上下文对象;
  2. 提供数据:通过 Context.Provider 包裹组件树,用 value 传递数据;
  3. 消费数据:后代组件通过 useContext 钩子或 Context.Consumer 读取数据。

示例

// 1. 创建 Context(可单独抽离为文件)
import { createContext, useContext, useState } from "react";const MyContext = createContext(); // 创建上下文// 2. 提供数据(祖先组件)
function Grandparent() {const [globalData, setGlobalData] = useState("全局数据");return (// Provider 包裹子树,value 传递数据(可包含更新函数)<MyContext.Provider value={{ globalData, setGlobalData }}><Parent /></MyContext.Provider>);
}// 中间组件(无需传递 props)
function Parent() {return <Child />;
}// 3. 消费数据(深层子组件)
function Child() {// 通过 useContext 直接获取上下文数据const { globalData, setGlobalData } = useContext(MyContext);return (<div><p>子组件获取:{globalData}</p><button onClick={() => setGlobalData("更新后的全局数据")}>更新数据</button></div>);
}

适用场景:中小型应用的跨层级通信(避免过度使用,否则会导致组件耦合度升高)。

四、无关联组件通信:全局状态管理

对于完全无关联的组件(如不同路由页面、不同模块的组件),需使用全局状态管理工具,将状态抽离到全局 store 中,所有组件均可访问和修改。

常用方案:

  1. Redux / Redux Toolkit
    最流行的全局状态管理库,通过 store 存储全局状态,组件通过 dispatch 发送 action 修改状态,通过 useSelector 读取状态。
    核心流程
    • 定义 reducer(处理状态更新的纯函数);
    • 创建 store(集中管理状态);
    • Provider 包裹应用,让所有组件访问 store
    • 组件通过 useSelector 获取状态,useDispatch 发送 action
  2. MobX
    基于 “响应式编程” 的状态管理库,通过 observable 定义状态,action 修改状态,组件通过 observer 监听状态变化。
  3. React 18 + useReducer + Context
    轻量级方案,用 useReducer 管理复杂状态逻辑,配合 Context 实现全局访问(适合中小型应用,避免引入第三方库)。

Redux 示例(简化)

// 1. 定义 reducer 和初始状态
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {switch (action.type) {case "INCREMENT": return { ...state, count: state.count + 1 };default: return state;}
}// 2. 创建 store
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({ reducer: counterReducer });// 3. 用 Provider 包裹应用
import { Provider } from "react-redux";
function App() {return (<Provider store={store}><ComponentA /><ComponentB /></Provider>);
}// 4. 组件 A 读取状态
import { useSelector } from "react-redux";
function ComponentA() {const count = useSelector((state) => state.count);return <div>Count: {count}</div>;
}// 5. 组件 B 修改状态
import { useDispatch } from "react-redux";
function ComponentB() {const dispatch = useDispatch();return <button onClick={() => dispatch({ type: "INCREMENT" })}>加 1</button>;
}

五、其他通信方式

  1. 事件总线(Event Bus)
    基于发布 - 订阅模式(如使用 mitt 库),组件通过 “发布事件” 和 “订阅事件” 通信,适合简单场景。

    import mitt from "mitt";
    const emitter = mitt(); // 创建事件总线// 组件 A 发布事件
    function ComponentA() {return <button onClick={() => emitter.emit("msg", "Hello")}>发送</button>;
    }// 组件 B 订阅事件
    function ComponentB() {useEffect(() => {const handleMsg = (data) => console.log("接收:", data);emitter.on("msg", handleMsg); // 订阅return () => emitter.off("msg", handleMsg); // 取消订阅}, []);return null;
    }
    
  2. Ref 通信
    父组件通过 ref 直接访问子组件的实例或 DOM 元素,适合获取子组件的状态 / 方法(但会破坏组件封装,谨慎使用)。

    function Parent() {const childRef = useRef(null);return (<div><Child ref={childRef} /><button onClick={() => childRef.current.log()}>调用子组件方法</button></div>);
    }// 子组件需用 forwardRef 暴露 ref
    const Child = forwardRef((props, ref) => {const log = () => console.log("子组件方法");useImperativeHandle(ref, () => ({ log })); // 暴露指定方法return <div>子组件</div>;
    });
    

总结:如何选择通信方式?

组件关系推荐方式适用场景
父子组件props + 回调函数直接的父子关系,数据单向流动
兄弟组件父组件中转同一父组件下的简单通信
跨层级组件Context API中小型应用的深层级通信
无关联组件Redux / MobX / 全局状态大型应用的全局数据共享
简单非关联组件事件总线(mitt)小型应用,临时通信需求

核心原则:尽量使用简单的方式解决问题,避免过度设计(如小应用不必引入 Redux,用 Context 即可)。

react核心实现原理

记忆点:

React 的核心实现原理围绕 “高效更新 UI” 展开,通过声明式编程模型虚拟 DOM(Virtual DOM)Fiber 架构协调算法(Reconciliation) 等机制,解决了传统 DOM 操作效率低、状态管理复杂的问题。其核心目标是:在保证开发体验(简洁的声明式 API)的同时,最小化真实 DOM 操作(因为 DOM 操作是前端性能瓶颈之一)。

  1. 通过虚拟 DOM 减少真实 DOM 操作,用 JS 计算替代高成本 DOM 操作;

  2. 基于Fiber 架构实现可中断的渲染流程,配合优先级调度保证页面响应性;

  3. 高效的 Diff 算法计算虚拟 DOM 差异,最小化更新成本;

  4. 通过状态驱动声明式 API,简化 UI 开发逻辑,同时支持批量更新、并发更新等优化。

React 的核心实现原理围绕 “高效更新 UI” 展开,通过声明式编程模型虚拟 DOM(Virtual DOM)Fiber 架构协调算法(Reconciliation) 等机制,解决了传统 DOM 操作效率低、状态管理复杂的问题。其核心目标是:在保证开发体验(简洁的声明式 API)的同时,最小化真实 DOM 操作(因为 DOM 操作是前端性能瓶颈之一)。

一、核心设计理念:声明式编程与组件化

React 的核心思想是 “声明式描述 UI”,开发者只需关注 “UI 应该是什么样子”(基于当前状态),而非 “如何一步步更新 UI”(命令式)。这种模式依赖两大基础:

  1. 组件化:UI 被拆分为独立、可复用的组件(如 function Component()class Component),每个组件封装了自身的状态(state)和渲染逻辑(render)。
  2. 状态驱动:组件的 UI 由其内部状态(state)或外部传入的属性(props)决定,状态变化时,React 自动重新渲染组件。

二、虚拟 DOM(Virtual DOM):减少真实 DOM 操作

真实 DOM 操作(如创建、修改、删除节点)是性能密集型操作(涉及浏览器重排、重绘)。React 引入虚拟 DOM 作为中间层,优化这一过程。

1. 什么是虚拟 DOM?

虚拟 DOM 是用 JavaScript 对象描述真实 DOM 结构的轻量级副本。例如,一个真实的 <div class="box">Hello</div> 对应的虚拟 DOM 可能是:

{type: 'div',        // 标签类型props: { className: 'box' },  // 属性children: 'Hello'   // 子节点
}

组件的 render() 方法(或函数组件的返回值)本质上就是生成这样的虚拟 DOM 对象。

  1. 虚拟 DOM 的工作流程

当组件状态变化时,React 会经历以下步骤:

  1. 生成新虚拟 DOM:基于新状态重新执行 render,生成新的虚拟 DOM 树。
  2. 对比新旧虚拟 DOM:通过 “Diff 算法” 计算新旧虚拟 DOM 的差异(哪些节点需要新增、修改或删除)。
  3. 更新真实 DOM:只将计算出的 “最小差异” 应用到真实 DOM 上(而非重新渲染整个 DOM 树)。

这一过程的核心优势是:用 JavaScript 计算(低成本)替代大量真实 DOM 操作(高成本),从而提升性能。

三、Fiber 架构:解决渲染阻塞问题

React 16 引入 Fiber 架构,解决了大型应用中 “长时间渲染导致页面卡顿” 的问题。其核心是将渲染工作分解为可中断、可恢复的小单元,并支持优先级调度。

1. 为什么需要 Fiber?

在 Fiber 之前,React 的渲染过程是 “同步且不可中断的”:一旦开始渲染(从根组件到叶子组件递归对比虚拟 DOM),会占用主线程直到完成。如果组件层级很深(如 1000 层),这一过程可能耗时 100ms 以上,导致页面无法响应用户输入(如点击、滚动),产生卡顿。

2. Fiber 的核心设计

  • Fiber 节点:将虚拟 DOM 节点扩展为 “Fiber 节点”,每个节点对应一个工作单元,存储:
    • 组件类型、属性、子节点等信息;
    • 工作状态(是否已完成、依赖的优先级等);
    • 指针(用于连接成链表,支持中断后恢复)。
  • 工作循环(Work Loop):渲染过程被分为两个阶段,支持中断和恢复:
    1. 协调阶段(Reconciliation)
      • 遍历 Fiber 树,对比新旧节点,标记需要更新的节点(如 “新增”“删除”“修改”);
      • 此阶段的工作可被中断(如更高优先级的任务到来,如用户输入),中断后可从上次中断的节点继续。
    2. 提交阶段(Commit)
      • 执行真实 DOM 操作(根据协调阶段的标记更新 DOM);
      • 调用生命周期钩子(如 componentDidMountuseEffect);
      • 此阶段不可中断(避免 DOM 处于不一致状态)。

3. 优先级调度

Fiber 配合 Scheduler(调度器) 实现任务优先级:

  • 高优先级任务(如用户输入、动画)可中断低优先级任务(如列表渲染);
  • 空闲时再继续低优先级任务,保证页面响应性。

四、协调算法(Reconciliation):高效计算 DOM 差异

协调算法(又称 “Diff 算法”)是虚拟 DOM 对比的核心,目标是以最小成本找出新旧虚拟 DOM 树的差异。React 的 Diff 算法基于两个假设(实际开发中需遵循,否则可能影响性能):

  1. 同层节点类型不同,则直接替换:不跨层级对比节点(如一个 div 的子节点不会与 span 的子节点对比)。
  2. 同层节点类型相同,则通过 key 标识唯一性:用于列表节点的复用(避免错误复用导致的状态混乱)。

1. 树级对比(层级差异)

React 只对比同一层级的节点:

  • 若父节点类型不同(如 div 变为 span),则直接删除旧节点及其所有子节点,创建新节点。
  • 若父节点类型相同,则继续对比其子女节点。

这一策略将 Diff 复杂度从 O (n³)(全量对比)降低到 O (n)(线性对比)。

2. 列表节点对比(key 的作用)

对于列表节点(如 [<li />, <li />]),若没有 keykey 不稳定(如用索引 index 作为 key),React 可能错误复用节点,导致状态异常

示例

// 旧列表(无 key 或 key 为 index)
[<li>1</li>, <li>2</li>]// 新列表(在头部插入元素)
[<li>0</li>, <li>1</li>, <li>2</li>]

若无稳定 key,React 会认为 “1 变成 0,2 变成 1,新增 2”,导致所有节点被修改;而有稳定 key 时,React 能正确识别 “新增 0,1 和 2 未变”,仅新增一个节点。

3. 组件级对比

若两个组件的类型相同(如都是 User 组件),则复用组件实例,仅对比其 props 差异;若类型不同,则直接卸载旧组件,挂载新组件。

五、状态更新机制:从 setState 到并发更新

React 的状态更新是触发重新渲染的核心,其机制在不断优化(从同步更新到并发更新)。

1. 状态更新的基本流程

  • 调用 setState(类组件)或 setXxx(Hooks,如 useState 的更新函数)时,React 会将状态更新加入队列。
  • 触发 “调度”:Scheduler 决定何时执行更新(根据优先级)。
  • 进入协调阶段:重新计算虚拟 DOM 差异。
  • 进入提交阶段:更新真实 DOM 并执行副作用。

2. 批量更新(Batching)

React 会将多个连续的状态更新 “合并” 为一次渲染,减少不必要的 DOM 操作。例如:

// 类组件
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 }); 
// 最终 count 只 +1(合并为一次更新)// Hooks
const [count, setCount] = useState(0);
setCount(c => c + 1);
setCount(c => c + 1); 
// 最终 count 只 +1(合并)

3. 并发更新(React 18+)

React 18 引入 “并发更新”,允许同一时间存在多个版本的 UI 渲染(但只提交最终版本)。例如,在输入框打字时,高优先级的输入更新可以中断低优先级的列表渲染,避免输入卡顿。

六、Hooks 原理:函数组件的状态管理

React 16.8 引入的 Hooks(如 useStateuseEffect),让函数组件拥有了状态和副作用能力,其实现依赖链表存储调用顺序

  • Hooks 存储:每个函数组件对应一个 Hook 链表,useStateuseEffect 等 Hooks 按调用顺序依次存入链表。
  • 依赖数组useEffect(fn, [dep]) 中的依赖数组用于判断副作用是否需要重新执行(对比前后依赖是否变化)。
  • 规则限制:Hooks 必须在函数组件顶层调用(不能在条件 / 循环中),否则会破坏链表的调用顺序,导致状态错乱。

总结

React 的核心实现原理可概括为:

  1. 通过虚拟 DOM 减少真实 DOM 操作,用 JS 计算替代高成本 DOM 操作;
  2. 基于Fiber 架构实现可中断的渲染流程,配合优先级调度保证页面响应性;
  3. 高效的 Diff 算法计算虚拟 DOM 差异,最小化更新成本;
  4. 通过状态驱动声明式 API,简化 UI 开发逻辑,同时支持批量更新、并发更新等优化。

这些机制共同让 React 既能提供简洁的开发体验,又能在复杂应用中保持高性能。

React的事件机制

记忆点:

本质是通过 “将所有组件的事件处理函数统一委托给顶层容器节点”,利用 DOM 事件冒泡特性实现高效的事件管理。这种机制既减少了事件绑定的内存消耗,又简化了跨浏览器兼容处理,同时适配 React 组件化模型。

实现原理:核心依赖两个关键设计:统一的委托目标事件映射表

当用户触发一个事件(如点击按钮)时,React 事件委托机制的完整处理流程可分为 5 步,涉及原生事件冒泡React 事件分发两个阶段:

阶段 1:原生 DOM 事件冒泡

阶段 2:React 事件分发与处理

1.顶层委托节点捕获事件,捕获之后react做事件的分发

2.查找对应处理函数

3.创建合成事件并执行处理函数

关键细节:委托机制的特殊处理

1. 如何关联 DOM 元素与组件的事件处理函数?

唯一性:将 “标识 + 事件类型 + 处理函数” 存入事件映射表

2.事件冒泡的 “两层独立性”

3. 不支持冒泡的事件如何处理?

对于原生不支持冒泡的事件(如 focusblur),React 会采用事件捕获(而非冒泡)机制实现委托

React 事件系统的事件委托机制是其核心设计之一,本质是通过 “将所有组件的事件处理函数统一委托给顶层容器节点”,利用 DOM 事件冒泡特性实现高效的事件管理。这种机制既减少了事件绑定的内存消耗,又简化了跨浏览器兼容处理,同时适配 React 组件化模型。以下从实现原理、完整流程、核心细节、版本变化四个维度详细解析:

一、核心设计:为什么需要事件委托?

传统原生 DOM 事件处理中,开发者通常会为每个元素单独绑定事件(如 button.addEventListener('click', handler))。这种方式在 React 组件化场景下存在明显缺陷:

  • 性能问题:一个页面可能有上千个组件(如列表项),每个组件绑定事件会导致大量内存占用,且频繁的绑定 / 解绑(如组件挂载 / 卸载)会引发性能损耗。
  • 跨浏览器兼容:不同浏览器的事件模型存在差异(如 IE 的 attachEvent 与标准 addEventListener),单独处理成本高。
  • 组件动态性:React 组件会频繁更新(如列表项增删),动态元素的事件绑定需要手动维护,容易遗漏或重复绑定。

React 的事件委托机制正是为解决这些问题而生:将所有事件处理函数统一委托到一个顶层节点,利用事件冒泡特性,在顶层统一接收和分发事件

二、实现原理:委托目标与事件映射表

React 事件委托的核心依赖两个关键设计:统一的委托目标事件映射表

1. 统一的委托目标(Delegation Target)

React 会将所有事件的处理逻辑委托到一个顶层容器节点,而非在每个组件对应的 DOM 元素上直接绑定。这个委托目标在不同 React 版本中有所变化:

  • React 16 及之前:委托目标是 document(整个页面的根节点)。
  • React 17 及之后:委托目标改为 React 应用的挂载容器节点(如 div#root,即 ReactDOM.render(<App />, root) 中的 root)。

为什么 React 17 要修改委托目标?

  • 避免与其他框架(如 jQuery)在 document 上的事件处理冲突(其他框架可能也会在 document 绑定事件,导致事件顺序混乱)。
  • 支持同一页面中嵌入多个独立的 React 应用(每个应用的事件委托在自己的容器内,互不干扰)。

2. 事件映射表(Event Registry)

React 内部维护了一个事件映射表(可理解为一个键值对集合),用于记录 “DOM 元素 / 组件” 与 “事件处理函数” 的对应关系。当事件触发时,React 通过这个映射表找到对应的处理函数并执行。

映射表的核心信息包括:

  • 事件类型(如 clickchange);
  • 对应的 DOM 元素(或组件实例);
  • 开发者定义的事件处理函数(如 onClick 回调)。

三、完整流程:从事件触发到处理函数执行

当用户触发一个事件(如点击按钮)时,React 事件委托机制的完整处理流程可分为 5 步,涉及原生事件冒泡React 事件分发两个阶段:

阶段 1:原生 DOM 事件冒泡

  1. 用户触发事件:如点击 <button> 元素,浏览器生成原生事件对象(如 MouseEvent),并从触发元素(button)开始向上冒泡。
  2. 事件沿 DOM 树向上传播:按照原生 DOM 规则,事件依次经过父元素、祖父元素…… 最终到达 React 的委托目标(如 #rootdocument)。

阶段 2:React 事件分发与处理

  1. 顶层委托节点捕获事件:委托目标上的原生事件监听器(由 React 内部提前注册)捕获到冒泡上来的原生事件,将其传递给 React 的事件分发器(Event Dispatcher)
  2. 查找对应处理函数
    事件分发器根据以下信息在 “事件映射表” 中查找匹配的处理函数:
    • 原生事件的类型(如 'click');
    • 事件的原始触发元素(event.target,即被点击的 button)。
      (注:React 会通过 DOM 元素与组件的对应关系,找到该元素所属的组件及绑定的事件处理函数,如 onClick 回调。)
  3. 创建合成事件并执行处理函数
    • React 创建合成事件对象(SyntheticEvent),封装原生事件的属性和方法(如 targetstopPropagation),并统一跨浏览器差异。
    • 将合成事件作为参数,调用找到的事件处理函数(如开发者定义的 handleClick)。
    • 处理函数执行完成后,合成事件对象在 React 17 前会被回收复用(事件池机制),React 17 后则直接销毁。

四、关键细节:委托机制的特殊处理

1. 如何关联 DOM 元素与组件的事件处理函数?

当 React 渲染组件时(如执行 render 生成 DOM),会为每个绑定了事件(如 onClick)的 DOM 元素添加一个唯一标识(内部属性),并将 “标识 + 事件类型 + 处理函数” 存入事件映射表。当事件触发时,通过 event.target 的唯一标识即可在映射表中找到对应的处理函数。

2. 事件冒泡的 “两层独立性”

React 合成事件的 “冒泡” 与原生 DOM 事件的 “冒泡” 是两个独立的流程

  • 原生冒泡:从触发元素到委托目标(如 button → ... → #root),是浏览器原生行为。
  • 合成事件冒泡:在 React 事件系统内部模拟的冒泡(如从子组件到父组件),通过 e.stopPropagation() 可阻止。

注意

  • 调用合成事件的 e.stopPropagation() 只能阻止合成事件的冒泡(如子组件的 onClick 阻止父组件的 onClick),但无法阻止原生事件继续向浏览器顶层(如 window)冒泡。
  • 若在原生事件中调用 e.stopPropagation(),会阻止事件到达 React 委托目标,导致 React 事件处理函数无法触发

3. 不支持冒泡的事件如何处理?

对于原生不支持冒泡的事件(如 focusblur),React 会采用事件捕获(而非冒泡)机制实现委托,确保这些事件也能被顶层节点捕获并处理。

五、优势总结

React 事件委托机制的核心价值体现在:

  1. 性能优化:只需在顶层节点绑定少量事件监听器,替代大量元素的单独绑定,减少内存消耗和 DOM 操作。
  2. 简化管理:动态新增 / 删除组件时,无需手动绑定 / 解绑事件(映射表会自动更新)。
  3. 跨浏览器兼容:通过合成事件屏蔽浏览器差异,开发者无需关注 addEventListenerattachEvent 等兼容问题。
  4. 适配组件模型:事件处理函数自动绑定组件实例(如 this 指向组件),符合 React 组件化开发习惯。

总结

React 事件委托机制的本质是:以顶层容器为统一委托目标,利用 DOM 事件冒泡特性,通过事件映射表关联 DOM 元素与组件事件处理函数,最终实现高效、兼容、易维护的事件管理。这一机制是 React 事件系统高性能和易用性的核心保障,也是理解 React 事件行为(如冒泡、跨组件通信)的基础。

React Router 中hash与history得区别

在 React Router 中,hashhistory 是两种不同的路由模式,核心区别体现在 URL 表现形式、底层实现原理、兼容性及服务器配置等方面。以下是具体对比:

1. URL 表现形式

  • hash 模式
    URL 中会包含 #(哈希符),哈希后面的部分作为路由标识。
    例如:http://example.com/#/homehttp://example.com/#/user/123
    # 及其后面的内容不会被发送到服务器,仅作为浏览器端的路由标识。
  • history 模式
    URL 中没有 #,路由看起来像普通的 URL 路径。
    例如:http://example.com/homehttp://example.com/user/123
    这种模式的 URL 更符合用户对 “正常网址” 的认知。

2. 底层实现原理

  • hash 模式
    基于浏览器的 hashchange 事件实现。当 URL 中 # 后面的内容变化时,浏览器会触发 hashchange 事件,React Router 监听该事件并更新路由视图,不会导致页面重新加载
    原理:# 是浏览器的原生特性,用于定位页面内锚点,其变化不会触发 HTTP 请求。
  • history 模式
    基于 HTML5 的 History APIhistory.pushState()history.replaceState())实现。这些 API 允许在不刷新页面的情况下修改浏览器的历史记录和 URL,React Router 通过调用这些方法更新路由,并监听 popstate 事件(如用户点击前进 / 后退按钮)来同步视图。
    原理:通过 API 手动修改 URL 和历史记录,本质上是对浏览器历史栈的操作。

3. 兼容性

  • hash 模式
    兼容性更好,支持所有现代浏览器及旧版本浏览器(如 IE9 及以下),因为 # 是浏览器的原生特性,无需依赖高级 API。
  • history 模式
    依赖 HTML5 的 History API,因此兼容性稍差,不支持 IE9 及以下版本浏览器(这些浏览器没有实现 pushState 等方法)。

4. 服务器配置要求

  • hash 模式
    无需特殊配置服务器。因为 # 后面的内容不会被发送到服务器,无论用户访问 http://example.com/#/home 还是 http://example.com/#/user,服务器只会接收到 http://example.com 的请求,只需返回单页应用的入口 HTML 文件(如 index.html)即可。
  • history 模式
    需要服务器额外配置。因为当用户直接访问 http://example.com/home 或刷新页面时,浏览器会向服务器发送 GET /home 的请求,若服务器没有配置对应路由,会返回 404 错误。
    解决方式:服务器需将所有路由请求(如 /home/user/* 等)都指向单页应用的入口 HTML 文件(index.html),让前端路由接管页面渲染。
    例如:Nginx 配置需添加 try_files $uri $uri/ /index.html;

5. 其他注意事项

  • SEO 友好性
    两者对 SEO 都有一定限制(单页应用的通病),但 history 模式的 URL 更 “正常”,搜索引擎可能更易识别(需配合服务端渲染 SSR 进一步优化)。
  • 路由参数传递
    两者都支持通过路由参数(如 /user/:id)传递数据,用法在 React Router 中一致,区别仅在于 URL 表现形式。
  • 哈希值的局限性
    hash 模式中,# 后面的内容会被浏览器视为 “片段标识符”,可能与页面内的锚点(如 <a href="#top">)冲突。

如何选择?

  • 若需要兼容旧浏览器(如 IE9),或不想配置服务器,选 hash 模式
  • 若追求更美观的 URL、更好的用户体验,且能控制服务器配置,选 history 模式(推荐现代应用使用)。

在 React Router 中,两种模式的使用方式也很简单:

  • hash 模式:通过 HashRouter 组件启用。
  • history 模式:通过 BrowserRouter 组件启用(默认模式)。

例如:

// hash 模式
import { HashRouter, Routes, Route } from 'react-router-dom';function App() {return (<HashRouter><Routes><Route path="/home" element={<Home />} /></Routes></HashRouter>);
}// history 模式(默认)
import { BrowserRouter, Routes, Route } from 'react-router-dom';function App() {return (<BrowserRouter><Routes><Route path="/home" element={<Home />} /></Routes></BrowserRouter>);
}

为什么let a = 1,修改a =2react不会重新渲染,需要通过useState触发重新渲染呢?

记忆点:React 只认自己 “管理的状态”,不认普通变量,心区别在于是否被 React 纳入 “可追踪的状态系统”

在 React 中,组件的重新渲染机制与 “状态追踪” 密切相关。let 声明的普通变量和 useState 创建的状态,核心区别在于是否被 React 纳入 “可追踪的状态系统”,这直接决定了变量修改后是否会触发组件重新渲染。

1. 普通变量(let a = 1)为什么不触发重新渲染?

let a = 1 声明的变量,本质上是JavaScript 环境中的普通变量,它的变化完全在 React 的 “感知范围之外”。

React 组件的渲染逻辑是:只有当组件的 “依赖项”(props 或 state)发生变化时,才会触发重新渲染。而普通变量的修改:

  • 不会被 React 记录到 “依赖追踪系统” 中;
  • 变量值变化后,React 完全不知情,自然不会触发组件的重新渲染;
  • 即使变量值变了,组件的 UI 也不会更新(因为组件不会重新执行渲染逻辑)。

例如:

function App() {let a = 1; // 普通变量const handleClick = () => {a = 2; // 修改变量,但 React 不知道console.log(a); // 控制台会输出 2,但 UI 不会变};return (<div><p>{a}</p> {/* 始终显示 1,因为组件没重新渲染 */}<button onClick={handleClick}>修改 a</button></div>);
}

点击按钮后,a 的值确实变成了 2,但组件没有重新渲染,所以页面上显示的还是初始值 1。

2. useState 为什么能触发重新渲染?

useState 是 React 提供的状态管理钩子,它的核心作用是将变量 “纳入 React 的状态系统”,让 React 能够追踪其变化。

当使用 useState 时:

  • React 会在组件内部创建一个 “状态容器”,存储该状态的当前值;

  • 当通过

    setXxx
    

    (状态更新函数)修改状态时,React 会:

    1. 更新 “状态容器” 中的值;
    2. 主动触发组件的重新渲染(调度一次更新);
    3. 重新执行组件函数时,useState 会返回最新的状态值,从而更新 UI。

例如:

import { useState } from 'react';function App() {const [a, setA] = useState(1); // 被 React 追踪的状态const handleClick = () => {setA(2); // 通知 React:状态变了,请重新渲染};return (<div><p>{a}</p> {/* 点击后会显示 2,因为组件重新渲染了 */}<button onClick={handleClick}>修改 a</button></div>);
}

点击按钮后,setA(2) 会告诉 React 状态已更新,React 会重新执行 App 组件,此时 useState 返回最新的 a = 2,UI 随之更新。

核心原因:React 的 “响应式设计”

React 并非自动追踪所有变量的变化(这会导致性能灾难),而是通过 useStateuseReducer 等 API 明确标记 “需要追踪的状态”。只有这些被标记的状态发生变化时,React 才会触发重新渲染,这是一种 “精确控制” 的设计。

普通变量的修改属于 “外部变化”,React 无法感知;而 useState 的更新函数会主动通知 React 状态变化,从而启动重新渲染流程。

简单说:React 只认自己 “管理的状态”,不认普通变量

Diff算法的实现

用于计算新旧虚拟dom树的最小差异

核心思想一致:通过优化对比策略降低复杂度,快速定位需要更新的节点

记忆点:

Diff 算法的核心是通过 “同层对比、类型优先、key 标识” 三大策略,将树形结构对比复杂度从 O (n³) 降至 O (n)

实现步骤:

1. 树级对比:快速排除跨层级差异

2. 组件级对比:复用相同类型组件

3. 列表节点对比:通过 key 复用节点(核心难点)

Diff 算法是虚拟 DOM 机制的核心,用于计算新旧虚拟 DOM 树的最小差异,最终只将这些差异应用到真实 DOM,从而减少不必要的 DOM 操作。不同框架(如 React、Vue)的 Diff 实现略有差异,但核心思想一致:通过优化对比策略降低复杂度,快速定位需要更新的节点

一、Diff 算法的核心优化策略

传统的树形结构全量对比(如递归对比所有节点)时间复杂度为 O(n³)(n 为节点数),难以满足前端高频更新需求。前端框架的 Diff 算法通过以下策略将复杂度优化至 O(n)(线性复杂度):

  1. 同层对比:只对比同一层级的节点,不跨层级比较(如不将父节点与子节点对比)。若父节点类型不同,直接判定 “整个子树需要替换”,无需深入子节点对比。
  2. 类型优先:若两个节点类型不同(如 divspan),直接判定 “旧节点需删除,新节点需创建”,不再对比子节点。
  3. 列表 key 标识:对于列表节点(如 [<li/>, <li/>]),通过 key 标识节点唯一性,快速定位可复用节点,避免错误复用导致的状态异常。

二、React Diff 算法的实现步骤

以 React 为例,Diff 算法的核心流程分为树级对比组件级对比列表节点对比三个层次,逐步缩小差异范围。

1. 树级对比:快速排除跨层级差异

树级对比是 Diff 的第一层检查,目标是快速判断 “是否需要全量替换子树”:

  • 遍历新旧虚拟 DOM 树的同一层级节点(如根节点的子节点、子节点的子节点等)。
  • 若当前层级的节点数量、类型差异较大(如旧节点是 div,新节点是 span),则直接标记 “删除旧节点及其所有子节点,创建新节点及其子节点”,终止该分支的深层对比。

示例

// 旧虚拟 DOM
{ type: 'div', children: [{ type: 'p', children: '旧内容' }] }// 新虚拟 DOM
{ type: 'span', children: [{ type: 'p', children: '新内容' }] }

树级对比发现根节点类型从 div 变为 span,直接判定 “删除旧 div 子树,创建新 span 子树”,无需对比子节点 p

2. 组件级对比:复用相同类型组件

当节点类型相同时(如都是 div 标签或都是 User 组件),进入组件级对比:

  • 原生标签(如 divp
    对比节点属性(props)的差异(如 classNamestyleonClick 等),标记 “需要更新的属性”(无需重新创建节点,只需更新属性)。
  • 自定义组件(如 User
    复用组件实例(避免重新初始化),仅对比 props 差异。若 props 相同,则组件不重新渲染;若 props 不同,触发组件的 render 方法生成新的子虚拟 DOM,继续递归对比。

示例

// 旧虚拟 DOM(自定义组件)
{ type: User, props: { name: '旧名' }, children: [] }// 新虚拟 DOM(自定义组件)
{ type: User, props: { name: '新名' }, children: [] }

组件类型相同,对比 props 发现 name 变化,触发 User 组件重新渲染,继续对比其内部子节点。

3. 列表节点对比:通过 key 复用节点(核心难点)

列表节点(如 ulli 子节点)是 Diff 中最复杂的场景,因为列表可能频繁增删、排序,需要高效定位可复用节点。React 通过 key 实现这一目标。

(1)无 key 或 key 不稳定(如 index)的问题

若列表节点无 key 或使用索引 index 作为 key,当列表变化时(如头部插入元素),React 会错误地复用节点,导致状态混乱。

示例

// 旧列表(key 为 index)
[<li key={0}>A</li>, <li key={1}>B</li>]// 新列表(头部插入 C)
[<li key={0}>C</li>, <li key={1}>A</li>, <li key={2}>B</li>]

React 会认为 “key=0 的节点从 A 变为 C”“key=1 的节点从 B 变为 A”,导致所有节点被重新渲染(而非仅新增 C)。

(2)key 为唯一标识时的对比逻辑

key 是稳定的唯一标识(如数据 ID)时,React 通过以下步骤对比列表:

  1. 构建旧节点 key 映射表:将旧列表节点按 key 存储为 { key: 节点 } 的映射(如 { 'a1': 节点A, 'b2': 节点B }),便于快速查找。

  2. 遍历新列表节点

    • 对每个新节点,用其 key 在旧映射表中查找对应的旧节点。
    • 若找到(key 匹配且类型相同):复用旧节点,对比并更新属性 / 子节点,记录节点位置变化(如需移动)。
    • 若未找到:标记 “需要创建新节点”。
  3. 清理未复用的旧节点:遍历旧映射表,标记所有未被新列表复用的节点为 “需要删除”。

(3)列表 Diff 的优化:减少移动操作

为进一步减少节点移动次数,React 会通过 “双指针法” 定位节点的最小移动范围:

  • 用两个指针(oldStartoldEnd 指向旧列表首尾,newStartnewEnd 指向新列表首尾)。
  • 优先对比首尾节点(如旧首 vs 新首、旧尾 vs 新尾),若匹配则直接复用,指针向中间移动;
  • 若首尾不匹配,再用 key 映射表查找,找到后移动节点,否则创建新节点。

这种策略能在多数场景下(如列表尾部新增 / 删除)避免全量遍历,进一步提升效率。

三、简化版 Diff 算法实现代码

以下是模拟 React Diff 核心逻辑的简化代码,展示如何对比两个虚拟 DOM 节点并生成差异:

// 虚拟 DOM 节点结构(简化)
// { type, key, props, children }/*** 对比新旧虚拟 DOM 节点,返回差异对象* @param {*} oldVNode 旧虚拟 DOM* @param {*} newVNode 新虚拟 DOM* @returns 差异对象 { type: 'REPLACE' | 'UPDATE' | 'MOVE' | 'REMOVE' | 'ADD', ... }*/
function diff(oldVNode, newVNode) {// 1. 新节点不存在:标记删除旧节点if (!newVNode) {return { type: 'REMOVE', vnode: oldVNode };}// 2. 旧节点不存在:标记新增新节点if (!oldVNode) {return { type: 'ADD', vnode: newVNode };}// 3. 节点类型不同(如 div  vs span,或不同组件):标记替换if (oldVNode.type !== newVNode.type) {return { type: 'REPLACE', oldVNode, newVNode };}// 4. 节点类型相同且是文本节点:对比内容if (typeof oldVNode === 'string' && typeof newVNode === 'string') {if (oldVNode !== newVNode) {return { type: 'UPDATE', content: newVNode };}return null; // 无差异}// 5. 节点类型相同且是元素/组件:对比 props 和 childrenconst diffResult = { type: 'UPDATE', props: {}, children: [] };let hasDiff = false;// 5.1 对比 props 差异const allProps = { ...oldVNode.props, ...newVNode.props };Object.keys(allProps).forEach(key => {const oldVal = oldVNode.props[key];const newVal = newVNode.props[key];if (oldVal !== newVal) {diffResult.props[key] = newVal;hasDiff = true;}});// 5.2 对比子节点(列表 Diff)const oldChildren = oldVNode.children || [];const newChildren = newVNode.children || [];const oldKeyMap = createKeyMap(oldChildren); // 构建旧子节点 key 映射// 遍历新子节点,查找可复用旧节点newChildren.forEach((newChild, newIndex) => {const key = newChild.key;if (key) {const oldChild = oldKeyMap[key];if (oldChild) {// 递归对比子节点差异const childDiff = diff(oldChild, newChild);if (childDiff) {diffResult.children.push({index: newIndex,diff: childDiff,key: key});hasDiff = true;}delete oldKeyMap[key]; // 标记为已复用} else {// 新节点,无对应旧节点:新增diffResult.children.push({index: newIndex,diff: { type: 'ADD', vnode: newChild },key: key});hasDiff = true;}} else {// 无 key:简单对比同位置节点(不推荐,仅作示例)const oldChild = oldChildren[newIndex];const childDiff = diff(oldChild, newChild);if (childDiff) {diffResult.children.push({ index: newIndex, diff: childDiff });hasDiff = true;}}});// 剩余未复用的旧节点:标记删除Object.values(oldKeyMap).forEach(oldChild => {diffResult.children.push({diff: { type: 'REMOVE', vnode: oldChild }});hasDiff = true;});return hasDiff ? diffResult : null;
}// 辅助函数:创建旧子节点的 key 映射表
function createKeyMap(children) {const map = {};children.forEach(child => {if (child.key) {map[child.key] = child;}});return map;
}

四、Diff 算法的意义与局限

  • 意义:通过高效对比定位最小差异,将 DOM 操作从 “全量更新” 变为 “精准更新”,大幅减少重排 / 重绘,是虚拟 DOM 性能优势的核心保障。
  • 局限:
    1. 额外的 JavaScript 计算开销(但通常远小于 DOM 操作开销);
    2. 列表 key 设计不当(如用 index)会导致 Diff 效率下降甚至状态异常;
    3. 极端场景(如完全逆序的列表)仍可能产生较多移动操作(部分框架通过 “最长递增子序列” 算法进一步优化)。

总结

Diff 算法的核心是通过 “同层对比、类型优先、key 标识” 三大策略,将树形结构对比复杂度从 O (n³) 降至 O (n),高效计算新旧虚拟 DOM 的差异。其中,列表节点的 key 处理是实现难点,合理使用稳定唯一的 key 是保证 Diff 效率和正确性的关键。理解 Diff 算法有助于优化组件渲染性能(如避免不必要的节点类型变化、合理设计 key 等)。

虚拟dom

虚拟 DOM(Virtual DOM)是前端框架(如 React、Vue)中用于优化 DOM 操作的核心技术,本质是用 JavaScript 对象描述真实 DOM 结构的轻量级副本。它通过减少直接操作真实 DOM 的次数(因为 DOM 操作是前端性能瓶颈之一),大幅提升页面渲染效率。

一、为什么需要虚拟 DOM?

真实 DOM 是浏览器渲染引擎管理的节点,包含大量属性和方法(如 parentNodestylegetBoundingClientRect 等),操作真实 DOM 会触发浏览器的重排(Reflow)重绘(Repaint),性能开销极大。

例如,直接修改一个列表中 1000 个元素的文本,会触发 1000 次真实 DOM 操作和多次重排;而通过虚拟 DOM,可先在 JavaScript 中计算所有修改,再一次性更新真实 DOM,仅触发少数几次重排。

二、虚拟 DOM 的本质:JavaScript 对象

虚拟 DOM 是对真实 DOM 的结构化描述,用简单的 JavaScript 对象表示节点的类型、属性、子节点等信息。

示例
一个真实的 DOM 节点:

<div class="container" id="app"><p>Hello</p><button onclick="handleClick()">Click me</button>
</div>

对应的虚拟 DOM 对象(简化版):

{type: 'div',                // 节点类型(标签名或组件名)props: {                    // 节点属性className: 'container',   // 对应 class(避免与 JS 关键字冲突)id: 'app'},children: [                 // 子节点列表(同样是虚拟 DOM 对象){ type: 'p', props: {}, children: 'Hello' },{ type: 'button', props: { onClick: handleClick }, children: 'Click me' }]
}

在 React 中,组件的 render() 方法(或函数组件的返回值)本质上就是生成这样的虚拟 DOM 对象(React 中称为 ReactElement)。

三、虚拟 DOM 的工作流程

当组件状态(如 stateprops)变化时,虚拟 DOM 的工作流程可分为 “生成新树→对比差异→更新真实 DOM” 三步:

1. 生成新虚拟 DOM 树(Render 阶段)

状态变化时,组件会重新执行渲染逻辑(如 React 的 render 或函数组件的重新调用),生成新的虚拟 DOM 树

这一步是纯 JavaScript 计算,不涉及任何 DOM 操作,性能开销极低。

2. 对比新旧虚拟 DOM 树(Diff 阶段)

通过Diff 算法(又称 “协调算法”)对比新旧虚拟 DOM 树,计算出两者的最小差异(哪些节点需要新增、修改、删除)。

Diff 算法是虚拟 DOM 性能的核心,前端框架会通过优化策略降低对比复杂度:

  • 同层对比:只对比同一层级的节点(不跨层级比较),将复杂度从 O (n³)(全量对比)降至 O (n)(线性对比)。
  • key 优化:列表节点通过 key 标识唯一性,避免错误复用节点(如列表增删时,用 key 快速定位变化的节点)。
  • 类型判断:若节点类型(如 divspan)不同,直接判定为 “需替换”,不再深入对比子节点。

3. 应用差异到真实 DOM(Patch 阶段)

将 Diff 阶段计算出的 “差异” 批量应用到真实 DOM 上,只更新必要的节点(而非重新渲染整个 DOM 树),从而减少重排 / 重绘次数。

四、虚拟 DOM 的核心优势

  1. 提升性能:用低成本的 JavaScript 计算替代高成本的 DOM 操作,减少重排 / 重绘。
  2. 简化开发:开发者只需描述 “UI 应该是什么样子”(基于状态),无需手动操作 DOM(框架自动处理差异更新)。
  3. 跨平台能力:虚拟 DOM 是与平台无关的 JavaScript 对象,可被渲染到不同平台(如 React 中,虚拟 DOM 可通过 ReactDOM 渲染到浏览器 DOM,通过 ReactNative 渲染到原生组件)。
  4. 批量更新:所有状态变化可先在虚拟 DOM 层累积,再一次性更新到真实 DOM,避免频繁的 DOM 操作。

五、虚拟 DOM 的局限性

  1. 额外的计算开销:生成虚拟 DOM 和 Diff 对比需要消耗 JavaScript 执行时间(但通常远小于直接操作 DOM 的开销)。
  2. 并非所有场景都更优:对于简单的、频繁更新的场景(如输入框实时校验),直接操作 DOM 可能比虚拟 DOM 更快(因此 Vue 提供了 v-directive、React 提供了 useRef 等绕过虚拟 DOM 的方案)。

六、虚拟 DOM 与真实 DOM 的核心区别

维度真实 DOM虚拟 DOM
本质浏览器渲染引擎管理的节点JavaScript 对象(内存中的数据结构)
操作成本高(触发重排 / 重绘)低(纯 JS 计算)
跨平台性依赖浏览器环境与平台无关(可渲染到任何环境)
属性复杂度包含大量浏览器内置属性和方法只包含必要的描述性属性(轻量)

总结

虚拟 DOM 是前端框架解决 “高效更新 UI” 问题的核心方案,其核心逻辑是:用 JavaScript 对象描述 DOM 结构,通过 Diff 算法计算最小更新差异,最终批量应用到真实 DOM。这一机制既降低了 DOM 操作的性能开销,又简化了开发流程,同时为跨平台渲染提供了基础。

理解虚拟 DOM 有助于深入掌握 React、Vue 等框架的渲染原理,以及在性能优化时做出更合理的决策(如合理使用 key、避免不必要的渲染等)。

React 渲染流程

记忆点:跟组件渲染流程差不多

从「状态更新」到「UI 最终呈现」的完整过程,主要分为 触发更新、协调(Reconciliation)、渲染(Rendering)、提交(Commit) 四个核心阶段。

  1. 点击按钮 → 调用 setCount(触发更新)。
  2. 协调阶段:
    • 重新执行组件函数,生成新的虚拟 DOM。
    • Diff 算法对比新旧虚拟 DOM,发现计数器文本变化。
    • Fiber 树标记该节点为「需要更新(Update)」。
  3. 渲染阶段:通知 ReactDOM 准备更新计数器节点。
  4. 提交阶段:
    • 修改真实 DOM 中计数器的文本。
    • 执行依赖 countuseEffect 回调。

React 的渲染流程是从「状态更新」到「UI 最终呈现」的完整过程,主要分为 触发更新、协调(Reconciliation)、渲染(Rendering)、提交(Commit) 四个核心阶段。理解这一流程有助于优化组件性能、避免不必要的渲染。

1. 触发更新(Update Trigger)

渲染流程的起点是「状态变化」,常见触发场景包括:

  • 组件初始化(首次渲染)。
  • 组件内部 setState(类组件)或 setXxx(函数组件 useState)调用。
  • 父组件传递的 props 发生变化。
  • useReducer 触发的状态更新。

当状态变化时,React 会标记该组件为「需要更新」,并进入协调阶段。

2. 协调阶段(Reconciliation):计算差异(Diffing)

协调阶段是 React 的「核心优化阶段」,目的是找出新旧虚拟 DOM(Virtual DOM)的差异,确定需要更新的部分(最小化 DOM 操作)。

核心步骤:

  • 生成虚拟 DOM(Virtual DOM)
    虚拟 DOM 是 React 对真实 DOM 的轻量抽象(JavaScript 对象),描述了 UI 的结构和属性。
    当组件状态更新时,React 会调用组件的 render 方法(类组件)或重新执行函数组件,生成新的虚拟 DOM 树
  • Diff 算法:对比新旧虚拟 DOM
    React 通过高效的 Diff 算法对比新旧虚拟 DOM 树,找出差异(哪些节点需要新增、删除、修改)。
    Diff 算法的特点:
    1. 同层比较:只对比同一层级的节点(不跨层级比较,降低复杂度)。
    2. 列表节点用 key 区分:对于列表,通过 key 标识节点身份,避免因顺序变化导致的误判(key 不变则认为是同一节点,仅更新内容)。
    3. 类型判断:若节点类型(如 div vs span)不同,直接销毁旧节点并创建新节点(不深入比较子节点)。
  • 生成 Fiber 树
    React 16+ 引入「Fiber 架构」,将虚拟 DOM 的 Diff 过程拆分为可中断、可恢复的小单元(Fiber 节点)。每个 Fiber 节点对应一个组件或 DOM 元素,记录了节点的类型、属性、子节点、更新优先级等信息。
    协调阶段会构建「新的 Fiber 树」,标记出需要更新的节点(如 Placement 新增、Update 修改、Deletion 删除)。

3. 渲染阶段(Rendering):准备更新

协调阶段完成后,React 会根据 Fiber 树的标记,确定每个节点的具体更新操作,并通知「渲染器(Renderer)」。

  • 渲染器的作用:React 本身不直接操作 DOM,而是通过渲染器(如浏览器环境的 ReactDOM、移动平台的 React Native)处理具体平台的渲染逻辑。
  • 此阶段是「纯计算阶段」,不操作真实 DOM,可被浏览器的事件循环中断(优先处理用户输入等高频任务),保证 UI 响应流畅。

4. 提交阶段(Commit):更新真实 DOM

提交阶段是「实际操作真实 DOM」的阶段,不可中断,主要做三件事:

  1. 执行 DOM 操作
    根据 Fiber 树的标记,批量执行真实 DOM 的增删改(如 appendChildremoveChild、修改属性等),最小化 DOM 操作以提升性能。
  2. 执行副作用(Side Effects)
    • 类组件:调用 componentDidMount(首次渲染)、componentDidUpdate(更新)、componentWillUnmount(卸载)等生命周期方法。
    • 函数组件:执行 useEffectuseLayoutEffect 等钩子的回调(useLayoutEffect 在 DOM 更新后同步执行,useEffect 在浏览器绘制后异步执行)。
  3. 更新引用和状态
    更新组件的真实 DOM 引用(如 useRef),并标记更新完成。

总结:完整流程示例

以「点击按钮更新计数器」为例:

  1. 点击按钮 → 调用 setCount(触发更新)。
  2. 协调阶段:
    • 重新执行组件函数,生成新的虚拟 DOM。
    • Diff 算法对比新旧虚拟 DOM,发现计数器文本变化。
    • Fiber 树标记该节点为「需要更新(Update)」。
  3. 渲染阶段:通知 ReactDOM 准备更新计数器节点。
  4. 提交阶段:
    • 修改真实 DOM 中计数器的文本。
    • 执行依赖 countuseEffect 回调。

React 渲染流程的核心是「通过虚拟 DOM 和 Diff 算法减少真实 DOM 操作」,结合 Fiber 架构实现可中断的渲染,平衡性能与用户体验。

为什么要使用HOOK?

在 React 中,Hook 是 React 16.8 引入的特性,其核心目的是解决类组件(Class Component)的固有缺陷,同时让函数组件(Function Component)能够拥有状态管理和副作用处理能力,最终让代码更简洁、逻辑更清晰、复用更高效。

为什么需要 Hook?—— 类组件的痛点

在 Hook 出现之前,React 组件主要通过 “类组件” 实现复杂逻辑(如状态管理、生命周期),但类组件存在几个明显问题:

  1. 逻辑复用困难,容易导致 “嵌套地狱”

类组件中复用状态逻辑的主流方式是 高阶组件(HOC)Render Props,但这两种方式都会导致组件层级嵌套过深(“Wrapper Hell”)。

例如,用高阶组件复用 “权限校验” 和 “数据加载” 逻辑:

// 高阶组件嵌套示例
const EnhancedComponent = withAuth(withData(FooComponent));// 渲染时的实际结构(嵌套层级多,调试困难)
<WithAuth><WithData><FooComponent /></WithData>
</WithAuth>

而 Hook 允许直接在函数组件中提取和复用逻辑,无需修改组件结构:

// 用自定义 Hook 复用逻辑(无嵌套)
function FooComponent() {const { user } = useAuth(); // 复用权限逻辑const { data } = useData(); // 复用数据加载逻辑// ...
}
  1. 复杂组件难以维护,逻辑分散

类组件中,相关逻辑往往被拆分到不同的生命周期方法中,导致代码碎片化。

例如,一个 “数据请求 + 订阅事件 + 清理” 的逻辑:

class MyComponent extends React.Component {state = { data: null };componentDidMount() {// 1. 数据请求fetchData().then(data => this.setState({ data }));// 2. 订阅事件window.addEventListener('resize', this.handleResize);}componentDidUpdate(prevProps) {// 3. 依赖变化时重新请求数据(与 mount 时的请求逻辑分离)if (prevProps.id !== this.props.id) {fetchData().then(data => this.setState({ data }));}}componentWillUnmount() {// 4. 清理事件订阅(与 mount 时的订阅逻辑分离)window.removeEventListener('resize', this.handleResize);}// ...
}

相关的 “数据请求” 和 “事件订阅” 逻辑被拆到 3 个生命周期中,维护时需要在多个方法间跳转,容易遗漏逻辑(比如忘记清理)。

而 Hook 可以将相关逻辑聚合到一起(如 useEffect):

function MyComponent({ id }) {const [data, setData] = useState(null);// 数据请求 + 依赖处理(聚合相关逻辑)useEffect(() => {fetchData().then(data => setData(data));}, [id]); // 仅当 id 变化时重新执行// 事件订阅 + 清理(聚合相关逻辑)useEffect(() => {const handleResize = () => {/* ... */};window.addEventListener('resize', handleResize);// 清理函数(与订阅逻辑放在一起)return () => window.removeEventListener('resize', handleResize);}, []); // 仅执行一次// ...
}

逻辑按功能聚合,而非按生命周期拆分,代码可读性和可维护性大幅提升。

  1. 类组件的 “this” 绑定问题

类组件中,this 的指向是动态的,需要手动绑定(如在构造函数中 bind,或用箭头函数),否则容易出现 this 指向错误:

class MyComponent extends React.Component {state = { count: 0 };constructor(props) {super(props);// 必须手动绑定 this,否则调用时 this 为 undefinedthis.handleClick = this.handleClick.bind(this);}handleClick() {this.setState({ count: this.state.count + 1 });}render() {return <button onClick={this.handleClick}>点击</button>;}
}

而函数组件中没有 “this”,所有变量和函数的作用域明确,避免了 this 绑定的心智负担:

function MyComponent() {const [count, setCount] = useState(0);// 无需绑定 this,直接使用const handleClick = () => {setCount(count + 1);};return <button onClick={handleClick}>点击</button>;
}
  1. 函数组件的功能限制被打破

在 Hook 出现前,函数组件只能是 “无状态组件”(Stateless Component),无法拥有自己的状态或处理副作用(如数据请求、事件监听),只能依赖 props 渲染 UI。

Hook 让函数组件能够:

  • 通过 useState 拥有自己的状态;
  • 通过 useEffect 处理副作用(数据请求、事件订阅等);
  • 通过 useContext 消费上下文;
  • 通过 useReducer 处理复杂状态逻辑;
  • 等等。

如今,函数组件 + Hook 可以实现类组件的所有功能,且写法更简洁。

总结:Hook 的核心价值

  1. 简化逻辑复用:用自定义 Hook 替代高阶组件和 Render Props,避免组件嵌套,让逻辑复用更直观。
  2. 聚合相关逻辑:将分散在生命周期中的代码按功能聚合,提升复杂组件的可维护性。
  3. 消除 this 困扰:函数组件中无需处理 this 绑定,减少错误和冗余代码。
  4. 增强函数组件能力:让函数组件支持状态和副作用,成为 React 组件的主流编写方式。

简言之,Hook 让 React 代码更简洁、灵活、易于维护,是 React 开发范式的一次重要升级,目前已成为 React 开发的推荐方式。

什么是react得调度?怎么实现得?

React 调度的核心实现

React 的调度机制主要依赖两个核心部分:Scheduler 调度器Fiber 架构,二者配合实现 “可中断、可恢复、优先级驱动” 的任务执行。

  1. Scheduler 调度器:管理任务优先级与执行时机

React 内置了一个独立的 scheduler 包(可单独使用),负责:

  • 对任务进行优先级分级;
  • 按照优先级排序并调度任务执行;
  • 利用浏览器空闲时间执行低优先级任务,避免阻塞主线程。

(1)优先级分级

Scheduler 将任务分为不同优先级(从高到低):

  • Immediate:同步执行(最高优先级,如 flushSync 触发的更新);
  • UserBlocking:用户阻塞级(如点击、输入等交互,需在 250ms 内响应);
  • Normal:普通优先级(如网络请求后的 UI 更新);
  • Low:低优先级(如非紧急的数据计算);
  • Idle:空闲时执行(最低优先级,如日志上报,可延迟至浏览器完全空闲)。

优先级通过 “过期时间”(expiration time)量化:优先级越高,过期时间越近(即需要尽快执行)。

(2)时间切片(Time Slicing)

为避免长任务阻塞主线程,Scheduler 实现了 “时间切片”:将一个长任务拆分成多个小任务,每个小任务执行时间不超过5ms(约为单帧时间的 1/3),剩余时间交还给浏览器处理 UI 和用户事件。

实现依赖浏览器的 requestIdleCallback 模拟(原生 requestIdleCallback 兼容性差且触发频率低,React 用 setTimeoutrequestAnimationFrame 做了 polyfill):

  • 每次执行任务前,计算当前可用时间(deadline);
  • 若任务执行超过可用时间,立即暂停,登记下一次执行;
  • 等浏览器处理完其他任务后,再恢复执行剩余部分。

(3)任务队列与调度循环

  • Scheduler 维护一个优先级队列(最小堆实现),按任务过期时间排序;
  • 调度循环(workLoop)不断从队列中取出 “最紧急”(优先级最高)的任务执行;
  • 若执行中出现更高优先级的新任务,会中断当前任务,优先执行新任务(“抢占式调度”)。

2. Fiber 架构:支持任务的中断与恢复

Fiber 是 React 16 引入的新数据结构,本质是 “可中断的工作单元”。每个 Fiber 节点对应一个组件,存储了组件的类型、DOM 信息、更新状态等。

Fiber 架构为调度提供了基础:

  • 任务拆分:将组件树的渲染(reconciliation)过程拆分成单个 Fiber 节点的处理(如计算新状态、比较子节点);
  • 可中断与恢复:每个 Fiber 节点的处理可以被暂停、标记,后续恢复时从暂停处继续(通过 alternate 属性保存当前状态);
  • 优先级关联:每个 Fiber 节点会记录对应的任务优先级,方便调度器判断是否需要中断当前任务,执行更高优先级任务。

3. 调度流程(简化版)

当触发组件更新(如 setStateuseState 更新)时,React 的调度流程大致如下:

  1. 触发更新:生成更新任务,计算任务优先级(根据更新源,如用户交互触发高优先级);
  2. 提交任务:将任务交给 Scheduler,加入优先级队列;
  3. 调度执行:Scheduler 从队列中取出最高优先级任务,启动执行;
  4. Fiber 工作循环:
    • 执行当前 Fiber 节点的处理(如 beginWork);
    • 检查是否超时(超过时间切片)或有更高优先级任务;
    • 若需中断:保存当前 Fiber 状态,将控制权交回浏览器;
    • 若未中断:继续处理下一个 Fiber 节点,直到完成整棵树的协调(reconciliation);
  5. 提交阶段(Commit):完成协调后,一次性更新 DOM(此阶段不可中断,确保 UI 一致性)。

总结

React 的调度是 “优先级驱动 + 可中断执行” 的机制,核心依赖:

  • Scheduler:负责优先级管理、时间切片、任务队列调度,确保高优先级任务优先执行,避免阻塞主线程;
  • Fiber 架构:将渲染任务拆分为可中断的工作单元,支持任务的暂停、恢复和抢占,配合调度器实现灵活的执行控制。

这种机制让 React 能够在处理复杂 UI 更新时,依然保持流畅的用户交互体验,是 React 并发模式(Concurrent Mode)的核心基础。

Fiber

react Fiber怎么做让react性能进行了提升

记忆点:

解决了什么问题,解决了传统协调算法的性能瓶颈,同步递归,无法中断,无法优先级调度造成页面的卡顿问题

通过:增量渲染(Incremental Rendering)和优先级调度

核心优化:1. 任务分片与增量渲染2. 优先级调度3. 双缓存树(Double Buffering),4. 生命周期细化与副作用处理,5. 浏览器友好的调度策略

React Fiber 是 React 16.x 版本引入的协调算法(Reconciliation Algorithm)的重构,它通过增量渲染(Incremental Rendering)和优先级调度(Priority Scheduling)解决了传统协调算法的性能瓶颈,显著提升了大型应用的响应速度和用户体验。其核心优化机制如下:

1. 任务分片与增量渲染

传统 React 的协调过程是同步递归的,在复杂组件树中可能导致长时间阻塞主线程,引发页面卡顿。Fiber 将渲染任务拆分为多个小任务单元(Fiber 节点),允许浏览器在任务间隙处理高优先级事件(如用户输入、动画):

// 传统协调(同步递归,可能导致卡顿)
function reconcileChildren(currentChildren, newChildren) {// 递归比较所有子节点,直到完成
}// Fiber 协调(异步可中断,分阶段执行)
function workLoopConcurrent() {while (workInProgress !== null && !shouldYield()) {workInProgress = performUnitOfWork(workInProgress);}
}

2. 优先级调度

Fiber 为不同类型的更新分配优先级(如动画更新优先级高于数据加载),允许高优先级任务中断低优先级任务:

// 不同类型更新的优先级示例
const HIGH_PRIORITY = 1; // 如用户输入、动画
const LOW_PRIORITY = 5;  // 如数据获取、UI 渲染// 调度器可根据优先级暂停或恢复任务
scheduleCallback(LOW_PRIORITY, () => {// 低优先级更新任务
});

3. 双缓存树(Double Buffering)

Fiber 使用双缓存技术维护两棵 Fiber 树(current 和 workInProgress),在内存中完成所有变更计算后一次性提交,减少 DOM 操作次数:

// 当前渲染的树
let currentRoot = null;
// 正在构建的工作树
let workInProgressRoot = null;function commitRoot() {// 将内存中完成的变更一次性应用到 DOMcommitWork(workInProgressRoot);currentRoot = workInProgressRoot;
}

4. 生命周期细化与副作用处理

Fiber 将生命周期方法分为render 阶段(可中断)和commit 阶段(不可中断),并引入 useEffect 等 API 处理副作用,避免不必要的重复渲染:

// 渲染阶段(可中断,纯计算)
function render() {return <Component />;
}// 提交阶段(DOM 操作、副作用执行)
useEffect(() => {// 副作用操作(如订阅、DOM 更新)return () => cleanup; // 清理函数
}, [dependencies]);

5. 浏览器友好的调度策略

Fiber 利用 requestIdleCallbackMessageChannel 实现空闲时段渲染,确保在浏览器帧间隙执行低优先级任务:

// 使用 MessageChannel 实现微任务调度
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;function scheduleWork() {port.postMessage(null); // 在下一个空闲时段执行任务
}

性能提升的具体表现

  • 动画 / 交互流畅度:高优先级更新(如滚动、过渡)可立即执行,避免卡顿
  • 大型应用响应速度:复杂组件树的渲染被分片,用户操作能及时响应
  • 资源利用率:通过优先级调度,减少不必要的渲染开销

React Fiber 通过架构层面的重构,从根本上解决了传统协调算法的性能瓶颈,为现代 Web 应用提供了更高效、更流畅的用户体验。

react fiber为什么使用它,原理是什么

记忆点:解决了什么问题,解决了传统协调算法的性能瓶颈,同步递归,无法中断,无法优先级调度造成页面的卡顿问题

通过:增量渲染(Incremental Rendering)和优先级调度

Fiber 的核心原理:可中断的工作单元与优先级调度

1. Fiber 节点:工作单元的载体

2.工作循环(Work Loop):分阶段处理工作单元

(1)协调阶段(Reconciliation Phase):可中断的 “计算阶段”

(2)提交阶段(Commit Phase):不可中断的 “执行阶段”

3. 优先级调度:优先处理关键任务

(1)优先级划分

(2)调度机制

将渲染工作拆分为可中断的 “Fiber 工作单元”,通过链表结构实现非递归遍历,配合优先级调度机制,在保证 UI 正确更新的同时,优先响应高优先级任务(如用户输入),避免主线程阻塞

React Fiber 是 React 16 引入的新协调引擎(Reconciliation Engine),其核心目标是解决传统 React 架构中 “长时间渲染阻塞主线程” 的问题,让 React 应用在处理复杂 UI 更新时保持流畅的用户交互(如点击、滚动、动画等)。

一、为什么需要 Fiber?传统架构的痛点

在 Fiber 之前,React 使用栈式协调器(Stack Reconciler) 处理组件渲染。其工作方式是:从根组件开始,递归遍历所有子组件,同步执行 “Diff 对比→计算更新→渲染 DOM” 的全过程,一旦开始就无法中断 —— 如果组件树层级较深(如 1000 层),这一过程可能持续 100ms 以上(超过人眼感知的 16ms 阈值),导致主线程被阻塞,页面无法响应用户输入或动画,产生卡顿。

具体来说,传统架构的问题包括:

  1. 不可中断:渲染过程一旦开始,必须完整执行,无法被高优先级任务(如用户点击)打断。
  2. 递归调用栈限制:依赖 JavaScript 函数调用栈递归遍历组件树,层级过深可能导致栈溢出。
  3. 无法优先级调度:所有更新任务(如 UI 渲染、动画、用户输入)优先级相同,无法优先处理关键任务。

二、Fiber 的核心原理:可中断的工作单元与优先级调度

Fiber 的本质是将渲染工作分解为可中断、可恢复的 “小单元”,并通过优先级调度决定这些单元的执行顺序,从而避免主线程长时间阻塞。其核心设计包括以下几点:

1. Fiber 节点:工作单元的载体

Fiber 将每个组件(或 DOM 节点)抽象为一个Fiber 节点,每个节点代表一个 “工作单元”。Fiber 节点不仅包含组件的类型、属性、DOM 信息等,还添加了用于 “中断与恢复” 的关键信息:

const fiberNode = {type: 'div', // 组件类型(如 'div' 或自定义组件)props: { className: 'box' }, // 组件属性stateNode: document.createElement('div'), // 对应的真实 DOM 节点(或组件实例)// 以下为 Fiber 核心控制字段parent: parentFiber, // 父 Fiber 节点(构成树结构)child: childFiber, // 第一个子 Fiber 节点sibling: nextFiber, // 下一个兄弟 Fiber 节点(替代递归栈,实现链表遍历)alternate: currentFiber, // 指向当前节点的“备用节点”(用于存储新旧状态对比)effectTag: 'UPDATE', // 标记当前节点的更新类型(如更新、新增、删除)expirationTime: 100, // 优先级时间戳(用于判断任务优先级)// ... 其他状态字段
};

核心改进

  • 链表结构parent/child/sibling)替代递归栈,使遍历过程可中断(无需依赖函数调用栈)。
  • 每个 Fiber 节点对应一个 “工作单元”,工作单元的处理(如 Diff 对比、计算更新)可以独立完成,便于拆分和中断。

2. 工作循环(Work Loop):分阶段处理工作单元

Fiber 将渲染过程拆分为两个阶段,通过 “工作循环” 调度执行,支持中断和恢复:

(1)协调阶段(Reconciliation Phase):可中断的 “计算阶段”

  • 任务:遍历 Fiber 树(基于链表),对比新旧节点(Diff 算法),标记需要更新的节点(effectTag),并构建 “Effect List”(记录所有需要执行的 DOM 操作)。
  • 特点:纯 JavaScript 计算,可被中断(如更高优先级任务到来时暂停,保存当前进度)。中断后可从上次暂停的 Fiber 节点继续处理。

(2)提交阶段(Commit Phase):不可中断的 “执行阶段”

  • 任务:根据协调阶段生成的 “Effect List”,执行实际的 DOM 操作(新增、删除、修改节点),并调用组件生命周期钩子(如 componentDidMountuseEffect)。
  • 特点:直接操作 DOM 和用户可见的状态,不可中断(避免 DOM 处于不一致状态,影响用户体验)。

3. 优先级调度:优先处理关键任务

Fiber 配合 Scheduler(调度器) 实现任务优先级管理,确保高优先级任务(如用户输入、动画)优先执行,低优先级任务(如列表渲染)可被中断。

  • 优先级划分:React 定义了不同优先级的任务(从高到低):

    • 同步优先级(Synchronous):如用户输入回调,必须立即执行,不可中断。
    • 用户阻塞优先级(UserBlocking):如动画、滚动,需要在短时间内完成。
    • 正常优先级(Normal):如网络请求后的 UI 更新,可延迟但不太久。
    • 低优先级(Low):如非紧急的后台计算。
    • 空闲优先级(Idle):仅在浏览器空闲时执行(如日志上报)。
  • 调度机制

    1. 每次处理一个工作单元前,Scheduler 检查是否有更高优先级任务;
    2. 若有,暂停当前工作单元,保存进度,先处理高优先级任务;
    3. 高优先级任务完成后,从上次暂停的位置恢复低优先级任务。

    (底层依赖 requestIdleCallback 或模拟的空闲时间检测,利用浏览器主线程的空闲时段处理低优先级任务。)

三、Fiber 带来的核心价值

  1. 解决主线程阻塞:通过拆分工作单元和中断机制,避免长时间占用主线程,保证用户交互和动画的流畅性。
  2. 支持优先级调度:关键任务(如输入、动画)优先执行,提升应用响应速度。
  3. 为并发模式奠定基础:Fiber 的可中断特性是 React 并发模式(Concurrent Mode)的核心,支持 “同一时间存在多个 UI 版本”(如渲染一个版本的同时准备另一个版本),进一步优化用户体验。
  4. 避免栈溢出:用链表遍历替代递归栈,支持更深层级的组件树渲染。

总结

Fiber 是 React 为解决 “大型应用渲染卡顿” 问题而设计的新型协调引擎,其核心原理是:
将渲染工作拆分为可中断的 “Fiber 工作单元”,通过链表结构实现非递归遍历,配合优先级调度机制,在保证 UI 正确更新的同时,优先响应高优先级任务(如用户输入),避免主线程阻塞

这一设计从根本上改善了 React 应用的性能和用户体验,同时为后续的并发特性(如 Suspense、并发渲染)提供了基础。

react onClick事件可以中断嘛?

React 中的 onClick 事件处理函数本身的同步执行过程无法被中断,但它所触发的后续渲染更新(如状态变化引发的重新渲染)可以被中断,这与 React 的事件机制和 Fiber 架构密切相关。

React 中的 onClick 事件处理函数本身的同步执行过程无法被中断,但它所触发的后续渲染更新(如状态变化引发的重新渲染)可以被中断,这与 React 的事件机制和 Fiber 架构密切相关。具体可从以下两方面理解:

一、onClick 事件处理函数的执行:不可中断的同步过程

当用户触发 onClick 事件时,React 的事件处理流程是:

  1. 原生 DOM 事件冒泡至顶层委托节点,React 捕获事件并创建合成事件对象;
  2. 调用开发者定义的 onClick 处理函数(如 handleClick),此函数在主线程中同步执行。

由于 JavaScript 是单线程的,一旦 onClick 处理函数开始执行,在其完成前(或遇到异步操作前),主线程会被独占,无法被其他任务(包括高优先级任务)中断。例如

function handleClick() {// 模拟耗时的同步操作(如大量循环计算)for (let i = 0; i < 1000000000; i++) {} console.log("处理完成");
}<button onClick={handleClick}>点击</button>

点击按钮后,handleClick 中的循环会阻塞主线程,期间页面无法响应其他交互(如滚动、输入),直到循环完成。

二、onClick 触发的更新:可被中断的渲染过程

onClick 事件通常会触发状态更新(如 setStateuseState 的更新函数),而状态更新引发的组件重新渲染过程(协调阶段)是可以被中断的,这依赖于 Fiber 架构的特性:

  1. 状态更新进入调度队列:调用 setState 后,React 会将更新任务加入队列,并由 Scheduler(调度器)根据优先级排序。
  2. 协调阶段(Reconciliation)可中断:在 Fiber 架构中,重新渲染的 “协调阶段”(计算虚拟 DOM 差异)被拆分为多个小工作单元,执行过程中会定期检查是否有更高优先级任务(如用户输入、动画)。若有,则暂停当前更新,先处理高优先级任务,稍后再恢复。
  3. 提交阶段不可中断:最终将差异应用到真实 DOM 的 “提交阶段” 不可中断,但此阶段耗时通常极短。

例如,onClick 触发一个列表渲染的更新,若此时用户快速输入文字(高优先级任务),React 会中断列表渲染的协调过程,优先处理输入事件,避免输入卡顿。

三、如何避免 onClick 处理函数阻塞主线程?

onClick 处理函数包含耗时操作(如复杂计算),即使其执行不可中断,也可通过以下方式避免阻塞:

  1. 拆分耗时操作为异步任务:用

    setTimeout
    

    requestIdleCallback
    

    将同步计算拆分为小块,让主线程有间隙处理其他任务:

    function handleClick() {// 异步执行耗时操作,避免阻塞setTimeout(() => {const result = heavyCalculation(); // 耗时计算setData(result);}, 0);
    }
    
  2. 使用 Web Workers:将计算密集型任务放入 Web Worker 中执行(不阻塞主线程),完成后通过消息通知主线程更新状态。

总结

  • onClick 事件处理函数本身:作为同步 JavaScript 代码,执行过程不可中断,耗时操作会阻塞主线程。
  • onClick 触发的更新渲染:在 Fiber 架构下,更新的 “协调阶段” 可被高优先级任务中断,保证用户交互的流畅性。

实际开发中,应避免在 onClick 中编写过长的同步代码,通过异步化或多线程方案优化性能。

可中断的事件是那些

注意:在 React 官方文档和源码中,“渲染阶段(Render Phase)” 通常是对「协调阶段(Reconciliation)」的另一种描述(或包含协调阶段的核心工作)。它强调的是 “计算需要更新什么” 的过程,而非 “实际更新 DOM”。

记忆点:

协调阶段

以下操作在协调阶段执行,因此可能被中断:

  1. 组件的 render 方法:计算并返回虚拟 DOM 结构的过程。

  2. 函数组件的执行:整个函数组件的逻辑(包括 Hooks 调用,如 useStateuseEffect 的回调计算等)。

  3. 生命周期方法(类组件):

  4. 虚拟 DOM 的 Diff 算法执行:对比新旧 Fiber 树的差异。

提交阶段(Commit)

当协调阶段完成后,React 进入提交阶段(也叫 “提交阶段”),此阶段的工作是将协调阶段计算出的差异应用到真实 DOM 上,包括:

  • 插入、更新、删除真实 DOM 节点
  • 执行副作用(如 useEffect 的回调、类组件的 componentDidMount/componentDidUpdate/componentWillUnmount

这个阶段不可中断,原因是:

  1. 操作真实 DOM 是同步的,一旦开始就必须完成,否则会导致 DOM 状态不一致(如只插入了部分节点,导致 UI 错乱)。

  2. 副作用(如数据请求、事件监听)需要在 DOM 操作完成后执行,中断会导致逻辑错误(如监听还没绑定,用户操作就触发了)

在 React 中,可中断的过程主要集中在 “协调阶段(Reconciliation)”,这是 React 16 引入 Fiber 架构后实现的核心特性。其设计目的是让 React 能够在执行耗时任务时,响应更高优先级的操作(如用户输入、动画等),避免应用卡顿。

一、可中断的核心阶段:协调阶段(Reconciliation)

协调阶段(也叫 “渲染阶段”)的主要工作是:

  • 找出前后两次虚拟 DOM(Fiber 树)的差异(Diffing)
  • 确定哪些节点需要更新、新增或删除
  • 为这些节点标记对应的操作(如 “插入”“更新”“删除”)

这个阶段的所有工作都是 “可中断、可暂停、可恢复、甚至可放弃” 的,原因是

  1. Fiber 架构将整个任务拆分成了无数个小单元(Fiber 节点),每个单元对应一个组件或 DOM 节点的处理。
  2. 每个小单元执行完成后,React 会检查是否有更高优先级的任务(如用户点击、键盘输入等)。如果有,就暂停当前任务,先处理高优先级任务;待高优先级任务完成后,再恢复之前的任务。
  3. 此阶段不会直接操作真实 DOM,仅在内存中计算差异,因此即使中断也不会导致 UI 不一致。

二、属于可中断阶段的具体操作

以下操作在协调阶段执行,因此可能被中断:

  1. 组件的 render 方法:计算并返回虚拟 DOM 结构的过程。

  2. 函数组件的执行:整个函数组件的逻辑(包括 Hooks 调用,如 useStateuseEffect 的回调计算等)。

  3. 生命周期方法(类组件):

    • shouldComponentUpdate:判断组件是否需要更新的计算过程。
    • getDerivedStateFromProps:从 props 推导 state 的过程。
    • getSnapshotBeforeUpdate:在提交阶段前获取快照(严格来说属于协调阶段末尾,接近提交阶段,中断可能性低)。
  4. 虚拟 DOM 的 Diff 算法执行:对比新旧 Fiber 树的差异。

三、不可中断的阶段:提交阶段(Commit)

当协调阶段完成后,React 进入提交阶段(也叫 “提交阶段”),此阶段的工作是将协调阶段计算出的差异应用到真实 DOM 上,包括:

  • 插入、更新、删除真实 DOM 节点
  • 执行副作用(如 useEffect 的回调、类组件的 componentDidMount/componentDidUpdate/componentWillUnmount

这个阶段不可中断,原因是:

  1. 操作真实 DOM 是同步的,一旦开始就必须完成,否则会导致 DOM 状态不一致(如只插入了部分节点,导致 UI 错乱)。
  2. 副作用(如数据请求、事件监听)需要在 DOM 操作完成后执行,中断会导致逻辑错误(如监听还没绑定,用户操作就触发了)

四、为什么需要 “可中断”?

React 设计可中断的协调阶段,核心是为了保证应用的响应性。例如:

  • 当用户在输入框打字时(高优先级任务),如果 React 正在执行一个复杂的列表渲染(低优先级任务),可以暂停渲染,先处理输入事件,避免用户输入卡顿。
  • 动画帧更新(高优先级)可以打断耗时的组件计算,确保动画流畅。

总结

React 中只有协调阶段(计算差异、生成新 Fiber 树)是可中断的,而提交阶段(操作 DOM、执行副作用)不可中断。这种设计是 Fiber 架构的核心,通过 “时间切片”(Time Slicing)机制,让 React 能够在复杂场景下保持应用的响应性。

useEffect子组件重复渲染重复进行请求,怎么进行解决

在 React 中,子组件因useEffect导致重复渲染和重复请求,通常与依赖项处理、组件记忆化或数据缓存有关。以下是具体解决方法:

1. 优化useEffect的依赖项

useEffect会在依赖项变化时重新执行。若依赖项是每次渲染都会生成新引用的值(如对象、数组、匿名函数),会导致不必要的重复执行。

错误示例

// 子组件
function Child({ id }) {// 每次渲染都会创建新对象,导致useEffect重复执行const params = { id: id }; useEffect(() => {fetchData(params); // 重复请求}, [params]); // 错误:params引用每次都变
}

解决方法

  • 依赖项使用原始值(而非对象 / 数组)
  • useMemo记忆复杂依赖项
function Child({ id }) {// 用useMemo记忆对象,确保引用稳定const params = useMemo(() => ({ id }), [id]); useEffect(() => {fetchData(params); }, [params]); // 仅当id变化时执行
}

2. 防止子组件不必要的重渲染

父组件重渲染时,子组件可能被连带重渲染,导致useEffect重复触发。可通过以下方式优化:

(1)用React.memo记忆子组件

React.memo会浅比较 props,若 props 未变化则阻止子组件重渲染。

// 用React.memo包装子组件
const Child = React.memo(({ id, fetchData }) => {useEffect(() => {fetchData(id);}, [id, fetchData]);return <div>...</div>;
});

(2)用useCallback记忆父组件传递的函数

父组件的函数若未被记忆,每次渲染会生成新引用,导致子组件的fetchData props 变化,触发重渲染。

// 父组件
function Parent() {// 用useCallback记忆函数,确保引用稳定const fetchData = useCallback((id) => {// 请求逻辑}, []); // 依赖项为空,函数仅创建一次return <Child id={id} fetchData={fetchData} />;
}

3. 缓存请求结果,避免重复请求

即使useEffect执行,也可通过缓存避免重复请求相同数据(如用useState+ 条件判断,或专门的缓存库)。

示例:本地缓存请求结果

function Child({ id }) {const [data, setData] = useState(null);const [loading, setLoading] = useState(false);useEffect(() => {// 若已有数据,直接返回(缓存生效)if (data?.id === id) return;const fetchData = async () => {setLoading(true);try {const res = await api.get(`/data/${id}`);setData(res.data);} catch (err) {console.error(err);} finally {setLoading(false);}};fetchData();}, [id, data]); // 依赖id和当前数据return <div>{data?.content}</div>;
}

4. 清理未完成的请求

组件卸载或依赖项变化时,若前一次请求未完成,可能导致内存泄漏或错误。需在useEffect的清理函数中取消请求。

示例:用 AbortController 取消请求

useEffect(() => {const controller = new AbortController(); // 创建控制器const signal = controller.signal;const fetchData = async () => {try {const res = await fetch(`/data/${id}`, { signal }); // 关联信号const data = await res.json();setData(data);} catch (err) {if (err.name !== 'AbortError') { // 忽略主动取消的错误console.error(err);}}};fetchData();// 清理函数:取消未完成的请求return () => controller.abort();
}, [id]);

总结

解决步骤优先级:

  1. 检查并优化useEffect的依赖项(核心原因)
  2. React.memo+useCallback防止子组件无效重渲染
  3. 缓存请求结果(减少重复请求)
  4. 清理未完成的请求(避免副作用残留)

通过以上方法,可有效避免子组件因useEffect导致的重复渲染和请求问题。

什么是next.js它的最大得特点

Next.js 是一个基于 React 的全栈框架,由 Vercel 开发并维护,主要用于构建高性能的 Web 应用(包括网站、App、后端服务等)。它在 React 的基础上扩展了一系列核心功能,解决了 React 单页应用(SPA)在实际开发中面临的痛点(如 SEO 优化、首屏加载速度、路由配置等),同时提供了更简洁的开发体验。

Next.js 最大的特点(核心优势)

  1. 灵活的渲染策略(最核心特点)
    支持多种渲染方式,可根据场景灵活选择,解决了传统 React SPA 因客户端渲染(CSR)导致的 SEO 差、首屏加载慢等问题:
    • 服务器端渲染(SSR,Server-Side Rendering):每次用户请求时,在服务器端动态生成 HTML 并返回,确保首屏内容可被搜索引擎抓取,且加载速度快。
    • 静态站点生成(SSG,Static Site Generation):构建时预渲染 HTML 页面,部署后直接复用,性能最优(适合静态内容,如博客、文档)。
    • 增量静态再生(ISR,Incremental Static Regeneration):结合 SSG 和动态内容优势,预渲染的页面可在后台定期更新,无需重新部署整个站点(适合电商商品页等需动态更新的静态内容)。
    • 客户端渲染(CSR):保留 React 传统的客户端渲染能力,适合交互密集型页面。
  2. 基于文件系统的路由
    无需手动配置路由规则,通过文件和文件夹结构自动生成路由,大幅简化路由管理:
    • pagesapp 目录下创建文件(如 about.js),自动对应路由 /about
    • 支持动态路由(如 [id].js 对应 /post/123)、嵌套路由(通过文件夹层级)、路由拦截等高级功能。
  3. 全栈开发能力
    内置 API 路由,允许在同一项目中直接编写后端接口(无需单独搭建服务器):
    • pages/apiapp/api 目录下创建文件(如 user.js),自动成为接口 /api/user
    • 可直接操作数据库、处理请求逻辑,轻松实现前后端一体化开发。
  4. 性能优化开箱即用
    内置多种性能优化机制,无需手动配置:
    • 自动代码分割:按路由拆分代码,只加载当前页面所需资源;
    • 图像优化:通过 next/image 组件自动优化图片(压缩、懒加载、WebP 格式转换等);
    • 字体优化:通过 next/font 优化字体加载,避免布局偏移;
    • 缓存机制:支持客户端缓存、服务器缓存、CDN 缓存等多层级缓存策略。
  5. 开发体验友好
    • 内置热模块替换(HMR),开发时修改代码实时更新,无需手动刷新;
    • 支持 TypeScript 开箱即用,类型检查更便捷;
    • 与 Vercel 平台深度集成,部署流程简单(也支持其他平台如 AWS、Netlify 等)。

总结

Next.js 最大的价值在于将 React 的组件化开发与灵活的渲染策略、简化的路由管理、全栈能力结合,既保留了 React 的开发体验,又解决了实际生产中的性能、SEO、部署等核心问题,是构建现代 Web 应用的主流框架之一(尤其适合需要兼顾性能、SEO 和开发效率的场景)。

react19

React 19 目前(截至 2025 年)仍处于开发和预览阶段,其核心目标是在 React 18 并发渲染的基础上,进一步提升开发体验、简化状态管理,并增强与服务器组件(Server Components)的集成。以下是基于官方规划和已公布特性,总结的 React 19 与 React 18 的主要区别:

1. 状态管理简化:useStateuseReducer 增强

  • React 18
    useState 对复杂状态(如对象、数组)的更新需要显式合并(如 setState(prev => ({ ...prev, key: value }))),否则会覆盖其他字段;useReducer 虽然适合复杂状态,但写法相对繁琐。

  • React 19
    引入自动合并对象状态的能力,useState 可以直接更新对象的部分字段,无需手动展开:

    // React 19 中:无需手动合并对象
    const [user, setUser] = useState({ name: "Alice", age: 30 });// 直接更新部分字段(React 会自动合并)
    setUser({ age: 31 }); // 结果:{ name: "Alice", age: 31 }
    

    同时简化 useReducer 的使用,支持更灵活的状态更新逻辑。

2. 服务器组件(Server Components)稳定化

  • React 18
    服务器组件(RSC)处于实验性阶段,仅支持基础功能,且与客户端组件的交互、数据获取逻辑等存在较多限制,需要手动配置边界(如 'use client' 指令)。
  • React 19
    服务器组件正式稳定化,提供更完善的开发体验:
    • 简化 'use client''use server' 指令的使用,自动区分客户端 / 服务器组件边界;
    • 内置服务器组件的数据获取能力(如 use 钩子直接在服务器组件中处理异步数据),无需额外库;
    • 优化服务器组件与客户端组件的通信(如 props 传递更高效,避免不必要的序列化)。

3. 数据获取与 Suspense 深度整合

  • React 18
    Suspense 可用于数据加载,但需要配合第三方库(如 React Query、SWR)的 Suspense 模式,且对 “嵌套数据加载” 的支持有限,容易出现瀑布流请求问题。

  • React 19
    新增 use 钩子,原生支持在组件中直接处理异步数据,与 Suspense 深度整合:

    // React 19 中:直接在组件中处理异步数据
    function UserProfile({ userId }) {// use 钩子接收 Promise,自动配合 Suspense 处理加载状态const user = use(fetchUser(userId)); return <div>{user.name}</div>;
    }// 配合 Suspense 使用
    <Suspense fallback={<Spinner />}><UserProfile userId={123} />
    </Suspense>
    

    use 钩子解决了 React 18 中 “在组件内直接 await 会破坏并发渲染” 的问题,同时支持嵌套数据加载的并行处理,避免请求瀑布流。

4. 动作(Actions)与表单处理增强

  • React 18
    表单处理需要手动管理提交状态(如 isSubmitting)、错误处理,且与服务器交互时需手动编写异步逻辑(如 fetch + setState)。

  • React 19
    引入服务器动作(Server Actions),允许直接在客户端组件中调用服务器端函数,自动处理表单提交状态和错误:

    // 服务器端定义动作(.server.js)
    export async function submitForm(formData) {const name = formData.get('name');// 服务器端逻辑(如数据库操作)return { success: true };
    }// 客户端组件使用
    import { useFormState } from 'react-dom';
    import { submitForm } from './actions.server';function MyForm() {// 自动管理提交状态(isPending)和结果(result)const [result, formAction, isPending] = useFormState(submitForm, { success: false });return (<form action={formAction}><input name="name" /><button type="submit" disabled={isPending}>{isPending ? '提交中...' : '提交'}</button>{result.success && <p>提交成功!</p>}</form>);
    }
    

    服务器动作自动与 React 的并发渲染协调,提交过程中不会阻塞 UI 交互。

5. 废弃与简化 API

  • React 18
    存在部分冗余或易混淆的 API,如 useEffect 的清理函数执行时机在并发模式下需要额外注意,ReactDOM.findDOMNode 等不推荐使用的 API 仍未移除。
  • React 19
    • 废弃 useEffect 的 “延迟清理” 行为,使清理函数执行时机更直观(与组件卸载 / 依赖变化同步);
    • 移除 ReactDOM.findDOMNodeReact.createFactory 等过时 API;
    • 简化 Children 相关工具函数(如 React.Children.map 功能被原生数组方法替代)。

6. 性能优化:编译时优化与并发渲染增强

  • React 18
    并发渲染依赖运行时调度(如 setTimeout 模拟时间切片),对极端场景(如超大规模列表)的性能仍有优化空间。
  • React 19
    探索编译时优化(如通过 Babel 插件提前分析组件依赖,减少运行时计算),进一步提升并发渲染的效率;
    优化服务器渲染(SSR)与客户端 hydration 的协调,减少 “水合不匹配” 错误,提升首屏交互速度。

总结

React 19 是对 React 18 并发渲染架构的 “补全与深化”:

  • 核心围绕服务器组件稳定化数据获取原生支持,简化全栈开发流程;
  • 通过 use 钩子、自动状态合并、服务器动作等特性,减少样板代码,提升开发效率;
  • 同时延续 React 18 的性能优化方向,进一步增强并发渲染的实用性和稳定性。

目前 React 19 尚未发布正式版,以上特性可能随开发进程调整,建议关注官方文档获取最新信息。

react18

useEffect 严格模式下为什么初始化会执行两次

在 React 严格模式(StrictMode)下,组件初始化时 useEffect 执行两次是有意为之的设计,目的是帮助开发者发现潜在的副作用问题。这是 React 18 引入的特性,仅在开发环境且开启严格模式时生效,生产环境不会执行两次。

为什么会执行两次?

React 严格模式会在以下情况重复调用组件:

  1. 组件挂载时:首次渲染和立即重新渲染
  2. 每次状态更新时:模拟未来可能的优化行为

这种重复执行可以暴露以下问题:

  • 副作用没有正确清理(如未取消的订阅)
  • 依赖数组不正确
  • 非幂等的初始化逻辑(执行多次会产生问题)

示例说明

考虑以下组件:

function Example() {useEffect(() => {console.log('Effect 执行');return () => {console.log('Effect 清理');};}, []);return <div>示例组件</div>;
}

在严格模式下,控制台会输出:

Effect 执行
Effect 清理
Effect 执行

如何处理?

  1. 确保副作用是幂等的:多次执行不会产生额外影响
useEffect(() => {// 安全:多次调用 setItem 不会有问题localStorage.setItem('key', 'value');return () => {// 清理操作也应该是幂等的localStorage.removeItem('key');};
}, []);
  1. 使用状态变量避免重复操作
const [initialized, setInitialized] = useState(false);useEffect(() => {if (!initialized) {// 只执行一次的逻辑setInitialized(true);}
}, [initialized]);
  1. 检查清理逻辑
    确保所有副作用都有对应的清理函数,尤其是事件监听、订阅等:
useEffect(() => {const subscription = api.subscribe();return () => {subscription.unsubscribe(); // 确保取消订阅};
}, []);

总结

严格模式下的双重执行是一种开发工具,帮助你编写更健壮的组件。只要确保副作用是安全的(幂等且有清理),就不会影响生产环境。如果你的组件在双重执行下表现异常,这通常表明代码中存在需要修复的问题。

React18 主要更新

记忆点:并发渲染,新的根 API,自动批处理,Transitions API,Suspense 增强,useDeferredValue

  1. 并发渲染(Concurrent Rendering)
    引入并发渲染机制,允许 React 中断、暂停、恢复甚至放弃渲染工作,为复杂 UI 提供更好的响应性。

  2. 新的根 API
    替换 ReactDOM.render()createRoot(),支持并发特性:

    import { createRoot } from 'react-dom/client';
    const root = createRoot(document.getElementById('root'));
    root.render(<App />);
    
  3. 自动批处理(Automatic Batching)
    自动合并多个状态更新,减少不必要的重渲染:

    // React 18 中会合并为一次渲染
    setState1();
    setState2();
    
  4. Transitions API
    区分紧急和非紧急更新,避免 UI 阻塞:

    • 紧急更新:未被 startTransition 包裹的更新(如直接调用 setState/setXxx),React 会视为高优先级,立即调度执行,确保同步响应。
    • 非紧急更新:被 startTransition 包裹的更新,React 会标记为「过渡更新(Transition)」,视为低优先级。当有紧急更新时,React 会暂停或中断非紧急更新,优先处理紧急更新;当浏览器空闲时,再恢复非紧急更新的执行。
    import { useTransition } from 'react';
    const [isPending, startTransition] = useTransition();// 非紧急更新(如搜索输入后的列表过滤)
    startTransition(() => {setFilteredList(inputValue);
    });
    
  5. Suspense 增强
    支持在客户端渲染中使用 Suspense 配合代码分割:

    <Suspense fallback={<Spinner />}><LazyComponent />
    </Suspense>
    
  6. useDeferredValue
    为状态值创建延迟版本,优先保证 UI 响应性:

    const deferredValue = useDeferredValue(value);标记为低级任务
    

当原始状态快速变化时,延迟版本会 “滞后” 更新,优先保证高优先级操作(如用户输入)的响应性,避免因频繁渲染低优先级内容(如列表过滤结果)而阻塞 UI

react18 怎么做的并行渲染

允许 React 同时准备多个 UI 版本(如处理一个非紧急更新的同时,不阻塞紧急更新的渲染)

  1. 基础:并发根(Concurrent Root)的引入。:更新过程是「可中断、可恢复、可优先级排序」的,为并行渲染提供了基础架构。

  2. 核心:Fiber 架构与「多版本 UI 树」并行构建

    并行构建:当有多个更新(如一个紧急更新和一个非紧急更新)时,React 可以同时在不同的 workInProgress 树上处理它们(互不干扰),例如:

    • 高优先级更新(如输入框输入)在一棵 workInProgress 树上快速构建。
    • 低优先级更新(如列表过滤)在另一棵 workInProgress 树上缓慢构建。
      最终,React 会选择优先级最高的 workInProgress 树,将其提交到 DOM 并替换 current 树。
  3. 调度系统:优先级驱动的「中断与恢复」

  4. 并发特性 API:标记更新优先级,释放并行能力

  5. 保证一致性:仅提交最终有效的 UI 版本

从而大幅提升复杂场景下的用户体验。

React 18 的「并行渲染(Parallel Rendering)」是其并发特性(Concurrent Features)的核心能力,允许 React 同时准备多个 UI 版本(如处理一个非紧急更新的同时,不阻塞紧急更新的渲染),从而大幅提升复杂场景下的用户体验。其实现依赖于架构升级、调度机制和新 API 的协同工作,核心逻辑如下:

1. 基础:并发根(Concurrent Root)的引入

React 18 中,通过 createRoot 替代 ReactDOM.render 创建的根节点(称为「并发根」)是支持并行渲染的前提。

// React 18 前(同步根,不支持并行渲染)
ReactDOM.render(<App />, document.getElementById('root'));// React 18 后(并发根,支持并行渲染)
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
  • 同步根:更新过程是「同步且不可中断」的,一旦开始渲染,必须完成才能响应其他事件(如用户输入),可能导致卡顿。
  • 并发根:更新过程是「可中断、可恢复、可优先级排序」的,为并行渲染提供了基础架构。

2. 核心:Fiber 架构与「多版本 UI 树」并行构建

React 16 引入的 Fiber 架构在 React 18 中进一步优化,支持「同时构建多棵 Fiber 树」(即多版本 UI 描述),这是并行渲染的技术基石。

  • Fiber 节点:每个组件 / 元素对应一个 Fiber 节点,记录组件类型、属性、更新状态等信息,且通过指针串联成树。
  • 双缓存机制:React 维护两棵 Fiber 树:
    • current 树:对应当前已渲染到 DOM 的 UI 版本。
    • workInProgress 树:正在构建的新 UI 版本(可能有多个,对应不同优先级的更新)。
  • 并行构建:当有多个更新(如一个紧急更新和一个非紧急更新)时,React 可以同时在不同的 workInProgress 树上处理它们(互不干扰),例如:
    • 高优先级更新(如输入框输入)在一棵 workInProgress 树上快速构建。
    • 低优先级更新(如列表过滤)在另一棵 workInProgress 树上缓慢构建。
      最终,React 会选择优先级最高的 workInProgress 树,将其提交到 DOM 并替换 current 树。

3. 调度系统:优先级驱动的「中断与恢复」

并行渲染的关键是「优先级调度」—— React 能识别更新的优先级,高优先级更新可以打断低优先级更新,待高优先级任务完成后,再恢复低优先级任务(或直接丢弃过时的低优先级任务)。

  • 优先级分类(从高到低):
    • 同步优先级(如用户输入、点击事件的即时反馈)。
    • 用户阻塞优先级(如拖拽、滚动等连续交互)。
    • 过渡优先级(通过 useTransition 标记的非紧急更新)。
    • 空闲优先级(浏览器空闲时才执行的低优先级任务)。
  • 调度过程
    1. 当更新触发时,React 为其分配优先级。
    2. 高优先级更新会「抢占」主线程,中断正在进行的低优先级渲染(通过 Fiber 节点的指针记录进度)。
    3. 高优先级更新完成并提交到 DOM 后,React 检查低优先级任务是否仍有必要(如状态未被覆盖),若有则恢复渲染;若已过时则直接丢弃。

4. 并发特性 API:标记更新优先级,释放并行能力

React 18 提供了多个 API 让开发者显式标记更新优先级,从而引导 React 进行并行渲染:

  • useTransition / startTransition:将非紧急更新标记为「过渡更新」(低优先级),允许其被高优先级更新打断。

    const [isPending, startTransition] = useTransition();
    // 紧急更新:输入框内容(立即响应)
    setInputValue(e.target.value);
    // 非紧急更新:列表过滤(可被打断)
    startTransition(() => {setFilteredList(filterData(e.target.value));
    });
    
  • useDeferredValue:为状态创建「延迟版本」,延迟版本的更新优先级低于原始状态,适用于基于现有状态派生的低优先级渲染。

    const deferredValue = useDeferredValue(inputValue);
    // 用延迟版本驱动低优先级渲染(列表过滤)
    const filteredList = filterData(deferredValue);
    
  • Suspense 与并发数据获取:配合 Suspense 时,React 可以在等待数据加载的同时,并行处理其他 UI 部分的渲染,避免整个页面阻塞。

5. 保证一致性:仅提交最终有效的 UI 版本

并行渲染中,React 可能同时构建多版本 UI,但最终只会将「最高优先级且最新」的版本提交到 DOM,确保用户看到的 UI 始终是一致的

  • 若低优先级更新在被打断后状态已过时(如用户输入已变化),React 会直接丢弃该版本的 workInProgress 树,避免无效渲染。
  • 提交到 DOM 的过程是「原子性」的(不可中断),确保用户不会看到不完整或不一致的 UI(如部分更新的列表)。

总结:React 18 并行渲染的核心逻辑

  1. 并发根为基础createRoot 启用并发模式,允许渲染过程可中断。
  2. Fiber 树多版本并行构建:通过多棵 workInProgress 树同时处理不同优先级的更新。
  3. 优先级调度:高优先级更新可打断低优先级更新,保证即时交互响应。
  4. 开发者通过 API 引导useTransition 等 API 标记非紧急更新,释放并行能力。
  5. 最终一致性保障:仅提交有效版本,避免 UI 错乱。

通过这套机制,React 18 能在复杂场景下(如快速输入 + 实时筛选)同时处理多个渲染任务,既保证用户操作的即时反馈,又不阻塞后续计算,显著提升了应用的响应性。

并发模式下React可能会渲染多次,如何避免这种情况?

并发模式下的 “多次渲染” 是 React 为了实现优先级调度而设计的正常行为,核心目标不是 “避免”,而是 “控制其影响”

  • 通过 React.memouseMemouseCallback 减少不必要的渲染触发;
  • 确保 render 纯函数特性和副作用幂等性,避免多次执行导致错误;
  • useTransition 等 API 帮助 React 优化渲染调度,减少无效重试。

合理利用这些方法,既能保留并发模式的性能优势(流畅的用户交互),又能避免多次渲染带来的问题。

调用的是浏览器的哪个API实现的

记忆点:

React 18 并非直接依赖单一浏览器 API 实现并行渲染,而是结合:

  • requestIdleCallback 利用浏览器空闲时间处理低优先级任务;

  • setTimeout 模拟时间切片,实现任务中断与让出主线程;

  • 事件循环机制 实现不同优先级任务的调度。

React 18 的并行渲染(并发模式)能够实现任务中断与优先级调度,其核心依赖于浏览器的 requestIdleCallback API 以及模拟时间切片的 setTimeout,同时结合了 JavaScript 的单线程事件循环机制。

关键浏览器 API 及工作原理

  1. requestIdleCallback
    这是浏览器提供的一个 API,允许开发者在浏览器空闲时间执行低优先级任务,不会阻塞主线程(如用户输入、动画等关键操作)。
    • 工作方式:浏览器在完成高优先级任务(如布局、绘制、用户交互事件)后,若有剩余时间,会调用 requestIdleCallback 注册的回调函数。
    • 在 React 中,低优先级任务(如通过 useTransition 标记的更新)会被安排在 requestIdleCallback 中执行,确保高优先级任务优先处理。
    • 限制:requestIdleCallback 的执行时机不确定(取决于浏览器是否空闲),因此 React 还会结合其他机制确保任务不会无限延迟。
  2. setTimeout(模拟时间切片)
    由于 requestIdleCallback 存在兼容性和延迟不确定性问题,React 内部主要通过 setTimeout 模拟时间切片(Time Slicing)机制:
    • 工作方式:将渲染任务拆分为小单元(Fiber 节点),每处理一个单元后,通过 setTimeout(fn, 0) 让出主线程,检查是否有更高优先级任务(如用户输入)。
    • 若有高优先级任务,当前低优先级任务会被中断;若无,则继续处理下一个单元。这实现了 “可中断渲染” 的核心能力。
  3. 事件循环(Event Loop)与任务队列
    浏览器的事件循环机制是并发调度的基础:
    • 主线程按优先级处理任务队列(宏任务如 setTimeout、用户事件;微任务如 Promise.then)。
    • React 将不同优先级的更新包装为不同类型的任务(如高优先级的用户输入为同步任务,低优先级的渲染为宏任务),利用事件循环的调度规则实现优先级排序。

总结

React 18 并非直接依赖单一浏览器 API 实现并行渲染,而是结合:

  • requestIdleCallback 利用浏览器空闲时间处理低优先级任务;
  • setTimeout 模拟时间切片,实现任务中断与让出主线程;
  • 事件循环机制 实现不同优先级任务的调度。

这些机制共同作用,让 React 能够在单线程中模拟 “并发渲染”,既保证高优先级任务(如用户交互)的即时响应,又能在后台处理低优先级任务(如大量数据渲染)。

React 18 与 React 16 相比,在架构设计、核心特性和性能优化上有诸多突破性变化,这些变化主要围绕 “并发渲染”(Concurrent Rendering)展开,同时带来了更强大的调度能力和开发体验提升。以下是核心区别:

1. 渲染架构:从 “同步不可中断” 到 “并发可中断”

  • React 16
    基于 Fiber 架构实现了 “可拆分的渲染任务”,但渲染过程是同步且不可中断的。一旦开始渲染(如执行 rendersetState 触发的更新),会持续占用主线程直到完成,若任务耗时过长(如渲染大量数据),会导致用户输入、动画等交互卡顿。
  • React 18
    在 Fiber 架构基础上,正式引入并发渲染(Concurrent Rendering),渲染过程变得可中断、可恢复、可丢弃。React 可以同时 “准备” 多个 UI 版本(在内存中),并根据任务优先级(如用户输入 > 列表渲染)决定最终提交哪个版本到 DOM。
    这一架构升级是 React 18 所有新特性的基础。
  1. 根节点 API:从 ReactDOM.rendercreateRoot
  • React 16
    使用 ReactDOM.render 挂载根节点,此模式下默认启用 “同步渲染”,不支持并发特性:

    import ReactDOM from 'react-dom';
    ReactDOM.render(<App />, document.getElementById('root'));
    
  • React 18
    新增 ReactDOM.createRoot API,替代 ReactDOM.render默认启用并发渲染

    import ReactDOM from 'react-dom/client';
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(<App />);
    

    这一 API 变化是启用所有并发特性(如 useTransition、自动批处理)的前提。

  1. 状态更新:从 “强制同步” 到 “智能批处理”
  • React 16
    仅支持 “有限批处理”:同一同步事件(如点击事件)中的多个 setState 会被合并为一次渲染,但异步操作(如 setTimeoutPromise.then)中的更新会被拆分,导致多次渲染:

    // React 16 中:会触发 2 次渲染(setTimeout 中的更新不批处理)
    function handleClick() {setTimeout(() => {setCount(c => c + 1);setFlag(f => !f);}, 0);
    }
    
  • React 18
    引入自动批处理(Automatic Batching),无论更新在同步还是异步操作中(如 setTimeoutfetchuseEffect),都会自动合并为一次渲染,减少不必要的重绘:

    // React 18 中:仅触发 1 次渲染(自动批处理)
    function handleClick() {setTimeout(() => {setCount(c => c + 1);setFlag(f => !f);}, 0);
    }
    

    同时提供 flushSync API 强制立即执行更新(跳出批处理)。

  1. 任务优先级:新增 “并发特性 API”

React 16 中所有更新优先级相同,无法区分 “紧急任务”(如用户输入)和 “非紧急任务”(如列表渲染),导致高优先级任务可能被低优先级任务阻塞。

React 18 新增了一系列 API 用于标记任务优先级,配合并发渲染实现 “优先级调度”:

  • useTransition:标记状态更新为 “低优先级”,不阻塞用户交互;
  • useDeferredValue:为值创建 “延迟版本”,优先保证 UI 响应性;
  • startTransition:独立函数,用于标记非紧急更新(如搜索联想、大数据过滤)。

示例(useTransition 避免输入卡顿):

// React 18 中:输入框更新(高优先级)不会被列表渲染(低优先级)阻塞
function Search() {const [input, setInput] = useState("");const [query, setQuery] = useState("");const [isPending, startTransition] = useTransition();const handleChange = (e) => {setInput(e.target.value); // 高优先级:立即响应startTransition(() => {setQuery(e.target.value); // 低优先级:后台处理,可被中断});};return (<div><input value={input} onChange={handleChange} />{isPending ? <Spinner /> : <Results query={query} />}</div>);
}
  1. Suspense 能力扩展
  • React 16
    仅支持 Suspense 配合 React.lazy 实现 “代码分割”(懒加载组件),且功能有限(如不支持数据加载场景):

    const LazyComponent = React.lazy(() => import('./LazyComponent'));
    // React 16 中:仅在组件加载时显示 fallback
    <Suspense fallback={<Loading />}><LazyComponent />
    </Suspense>
    
  • React 18
    大幅增强 Suspense 能力,支持数据加载场景(配合 Suspense 兼容的数据库,如 React Query、SWR),且在并发渲染下,Suspense 可以暂停某个组件的渲染,同时不阻塞其他组件更新:

    // React 18 中:数据加载时显示 fallback,且不阻塞其他 UI
    <Suspense fallback={<Spinner />}><DataComponent /> {/* 组件内部通过 Suspense 兼容的方式请求数据 */}
    </Suspense>
    

    同时支持 “流式服务器端渲染(Streaming SSR)”,可逐步发送 HTML 内容,提升首屏加载体验。

  1. 其他重要变化
  • 服务器组件(Server Components)支持:React 18 开始实验性支持服务器组件,允许组件在服务器端渲染(不生成客户端 JS),大幅减少客户端 JavaScript 体积(React 16 无此特性)。
  • 严格模式增强:开发环境下,React 18 会对组件进行两次渲染(React 16 只渲染一次),以检测 “非纯函数” 副作用(如 render 中执行 API 调用),提前暴露并发模式下的潜在问题。
  • 生命周期与钩子调整useEffect 的清理函数执行时机更稳定,避免 React 16 中可能出现的 “竞态条件”;getSnapshotBeforeUpdate 等生命周期的行为更符合预期。

总结

React 18 是对 React 16 架构的一次 “质变” 升级:

  • 核心目标是通过并发渲染实现 “用户交互优先”,解决 React 16 中同步渲染导致的卡顿问题;
  • 新增的 createRootuseTransition、自动批处理等特性,让开发者能更轻松地构建高性能、高响应性的应用;
  • 同时保持了对 React 16 代码的兼容性(大部分应用只需替换 ReactDOM.render 即可升级)。

这些变化使得 React 18 在复杂 UI 场景(如大数据列表、实时交互应用)中表现远超 React 16。

怎么判断react版本是否稳定呢

判断 React 版本是否稳定,可以通过以下几个权威渠道和标准来确认,确保你使用的是经过官方验证、适合生产环境的版本:

1. 查看官方发布页面(最权威)

React 的官方发布信息以 React 官方博客GitHub Releases 页面 为准:

  • 稳定版本*:标题中无 alphabetarc(Release Candidate,候选发布版)等标识,例如 v18.2.0v17.0.2 均为稳定版。
  • 非稳定版本:标题中会明确标注预发布标签,例如 v19.0.0-alpha.1(内部测试版)、v18.0.0-beta.3(公测版)、v19.0.0-rc.2(候选发布版),这些版本通常用于测试新特性,不建议用于生产环境。

2. 检查 npm 仓库的版本标签

React 作为 npm 包发布,可通过 npm 官网或命令行查看版本状态:

  • 访问 npm 上的 react 包页面,首页显示的 “Latest Version” 即为当前最新稳定版。
  • 命令行中运行 npm view react versions,可列出所有发布过的版本,末尾无预发布标签(alpha/beta/rc)的即为稳定版。

3. 参考官方文档的版本说明

React 官方文档(react.dev)会明确标注当前推荐的稳定版本,以及各版本的兼容性和支持状态:

  • 文档中提到的 “最新稳定版” 或 “推荐版本” 均经过充分测试,适合生产环境。
  • 对于非稳定版本(如 React 19 目前的预览版),官方会明确说明 “处于实验阶段”“不建议用于生产” 等提示。

4. 注意版本的 “LTS” 标识(长期支持版)

虽然 React 官方没有严格的 “LTS”(长期支持)概念,但通常:

  • 主版本号(如 18.x17.x)发布后,会持续推出小版本更新(如 18.1.018.2.0),修复 bug 和安全问题,这些小版本均为稳定版。
  • 主版本号迭代(如从 18 到 19)前,旧主版本会有一段时间的维护支持,确保稳定过渡。

总结

判断标准优先级:

  1. 官方博客和 GitHub Releases 中无 alpha/beta/rc 标签的版本 → 稳定版;
  2. npm 官网标注的 “Latest Version” → 最新稳定版;
  3. 官方文档推荐的版本 → 经过验证的稳定版。

生产环境应优先选择稳定版,避免使用带有预发布标签的版本,以确保应用的兼容性和安全性。

*react 的历史

https://www.yuque.com/ergz/web/sxnv2y

https://react.iamkasong.com/preparation/idea.html#react-%E7%90%86%E5%BF%B5

react hooks 原理

记忆点:

其核心目的是让函数组件能够拥有类组件的能力(如状态管理、生命周期副作用等),同时解决类组件中逻辑复用复杂(如 HOC 嵌套问题)、生命周期方法中逻辑分散等痛点。

原理:Hooks 的实现依赖于 React 内部的 Hooks 存储机制 和 函数组件的 Fiber 节点关联,核心可概括为:通过 “链表” 按顺序存储组件的 Hooks 信息,结合 Fiber 架构实现状态的保存与复用。

React Hooks 是 React 16.8 引入的特性,其核心目的是让函数组件能够拥有类组件的能力(如状态管理、生命周期副作用等),同时解决类组件中逻辑复用复杂(如 HOC 嵌套问题)、生命周期方法中逻辑分散等痛点。

一、Hooks 解决的核心问题

在 Hooks 出现前,函数组件是 “无状态” 的,只能接收 props 并返回 UI;类组件虽能管理状态和副作用,但存在以下问题:

  • 逻辑复用需通过高阶组件(HOC)或 render props,易形成 “嵌套地狱”;
  • 生命周期方法(如 componentDidMount)常混杂多个不相关逻辑(如数据请求、事件监听);
  • this 指向问题增加心智负担。

Hooks 通过将 “状态逻辑” 与 “UI 渲染” 分离,让函数组件可以:

  • 拥有自己的状态(useState);
  • 处理副作用(useEffect);
  • 复用状态逻辑(自定义 Hooks)。

二、Hooks 的核心原理

Hooks 的实现依赖于 React 内部的 Hooks 存储机制函数组件的 Fiber 节点关联,核心可概括为:通过 “链表” 按顺序存储组件的 Hooks 信息,结合 Fiber 架构实现状态的保存与复用

1. Hooks 的存储结构:链表

React 为每个函数组件的实例(或 Fiber 节点)维护了一个 Hooks 链表,用于存储该组件中所有 Hooks 的信息(如状态值、更新函数、依赖项等)。

  • 每个 Hook 对应一个链表节点(Hook 对象),结构大致如下(简化版):

    const hook = {memoizedState: null, // 当前 Hook 的状态值(如 useState 的 state)queue: null, // 状态更新队列(如 useState 的更新函数队列)next: null, // 指向链表中的下一个 Hook// 其他属性:如 useEffect 的回调、依赖项、清除函数等
    };
    
  • 组件渲染时,React 会通过一个 全局指针(workInProgressHook 遍历 Hooks 链表,按顺序读取或更新每个 Hook 的信息。

2. Hooks 调用规则的本质:顺序不可变

Hooks 必须在函数组件的 顶层作用域 调用(不能在条件、循环、嵌套函数中调用),这是因为:
Hooks 链表是按 “调用顺序” 建立的,每次渲染时,Hooks 的调用顺序必须与链表节点顺序一致

例如,一个组件的 Hooks 调用顺序为:

function Component() {const [a, setA] = useState(1); // 第 1 个 HookuseEffect(() => {}, []); // 第 2 个 Hookconst [b, setB] = useState(2); // 第 3 个 Hookreturn <div />;
}

首次渲染时,React 会创建 3 个 Hook 节点,形成 hook1 -> hook2 -> hook3 的链表。
若第二次渲染时,因条件判断跳过了 useEffect

function Component() {const [a, setA] = useState(1); // 第 1 个 Hook(正常)if (someCondition) {useEffect(() => {}, []); // 条件中调用,可能被跳过}const [b, setB] = useState(2); // 本应是第 3 个,却被当作第 2 个读取return <div />;
}

此时,useState(b) 会被误认为是第 2 个 Hook(实际是原第 3 个),导致读取到错误的 memoizedState,引发状态混乱。

3. useState 原理:状态的保存与更新

useState 是最基础的 Hook,用于在函数组件中声明状态。其核心逻辑可拆解为:

  • 初始化(首次渲染)
    当调用 useState(initialState) 时,React 会创建一个新的 Hook 节点:

    • memoizedState 设为初始值(initialState,支持函数形式延迟初始化);
    • queue 初始化一个更新队列(用于存储多次状态更新);
    • 将节点加入当前组件的 Hooks 链表,指针后移。
  • 状态更新
    调用 setState(更新函数)时,React 会将更新操作(如新值或更新函数)加入该 Hook 的 queue 队列,然后触发组件 重新渲染

  • 重新渲染时
    React 会重新遍历 Hooks 链表,按顺序找到对应的 Hook 节点,从 memoizedState 中读取最新状态(已通过队列计算更新后的值)。

    简化伪代码逻辑:

    function useState(initialState) {// 获取当前 Hook 节点(通过全局指针)const hook = currentHook;// 首次渲染:初始化状态if (isMount) {hook.memoizedState = typeof initialState === 'function' ? initialState() : initialState;hook.queue = { pending: null }; // 初始化更新队列}// 创建更新函数(setState)const setState = (action) => {// 将更新操作加入队列const update = { action, next: null };hook.queue.pending = update;// 触发组件重新渲染scheduleUpdate();};// 指针后移,准备处理下一个 HookcurrentHook = currentHook.next;return [hook.memoizedState, setState];
    }
    

4. useEffect 原理:副作用的管理

useEffect 用于处理组件的 “副作用”(如数据请求、事件监听、DOM 操作等),其核心是 区分 “挂载(mount)” 和 “更新(update)” 阶段,并按依赖项决定是否执行副作用

  • 依赖项机制
    useEffect(callback, deps) 的第二个参数 deps 是依赖项数组。React 会对比前后两次渲染的 deps

    • deps 不存在:每次渲染后都执行 callback
    • deps 为空数组 []:仅在组件挂载后执行一次(类似 componentDidMount);
    • deps 有值:仅当依赖项发生变化时,才执行 callback(先执行上次的清除函数,再执行新 callback)。
  • 执行时机
    useEffectcallback 会在 DOM 更新完成后 异步执行(不同于 useLayoutEffect 的同步执行),避免阻塞浏览器渲染。

  • 内部存储
    useEffect 对应的 Hook 节点会存储:

    • 上次的依赖项(prevDeps);
    • 副作用回调(callback);
    • 清除函数(callback 的返回值)。

    简化伪代码逻辑:

    function useEffect(callback, deps) {const hook = currentHook;if (isMount) {// 首次渲染:存储依赖和回调hook.memoizedState = { deps, callback, cleanup: null };// 安排在 DOM 更新后执行scheduleEffect(hook);} else {// 更新时:对比依赖const prevDeps = hook.memoizedState.deps;const hasChanged = deps.some((dep, i) => dep !== prevDeps[i]);if (hasChanged) {// 依赖变化:执行上次清除函数,更新回调和依赖hook.memoizedState.cleanup?.();hook.memoizedState = { deps, callback, cleanup: null };scheduleEffect(hook);}}currentHook = currentHook.next;
    }// 执行副作用(DOM 更新后)
    function runEffect(hook) {const cleanup = hook.memoizedState.callback();hook.memoizedState.cleanup = cleanup; // 保存清除函数
    }// 组件卸载时:执行所有清除函数
    function unmountComponent() {hooks.forEach(hook => hook.memoizedState.cleanup?.());
    }
    

5. 与 Fiber 架构的关联

React 16 引入的 Fiber 架构 是 Hooks 实现的基础。Fiber 是 React 内部对 “工作单元” 的抽象,每个函数组件对应一个 Fiber 节点。

  • 每个 Fiber 节点会通过 memoizedState 属性关联该组件的 Hooks 链表(即第一个 Hook 节点);
  • 组件重新渲染时,React 会通过 Fiber 节点找到对应的 Hooks 链表,确保状态能被正确复用;
  • Fiber 的 “可中断、可恢复” 特性,与 Hooks 按顺序读取的机制兼容,保证了渲染过程中状态的一致性。

三、自定义 Hooks 的本质

自定义 Hooks(如 useRequestuseLocalStorage)本质是 Hooks 的组合复用,其核心规则是:
自定义 Hooks 必须以 use 开头,内部可以调用其他 Hooks(包括内置 Hooks 和其他自定义 Hooks)

例如,一个封装 localStorage 状态的自定义 Hook:

function useLocalStorage(key, initialValue) {// 内部调用 useState 和 useEffectconst [value, setValue] = useState(() => {const item = localStorage.getItem(key);return item ? JSON.parse(item) : initialValue;});useEffect(() => {localStorage.setItem(key, JSON.stringify(value));}, [key, value]);return [value, setValue];
}

当组件调用 useLocalStorage 时,其内部的 useStateuseEffect 会被当作组件自身的 Hooks 加入链表,从而复用状态逻辑。

总结

React Hooks 的核心原理可概括为:

  1. 存储机制:通过 “链表” 按顺序存储组件的 Hooks 信息,依赖调用顺序实现状态复用;
  2. 状态管理useState 通过更新队列管理状态变化,触发组件重新渲染;
  3. 副作用处理useEffect 基于依赖项对比,在 DOM 更新后异步执行副作用,支持清除机制;
  4. 复用逻辑:自定义 Hooks 通过组合内置 Hooks,实现状态逻辑的抽离与复用。

这种设计让函数组件摆脱了类组件的束缚,同时保持了逻辑的清晰性和复用性,成为 React 开发的主流范式。

Redux

Redux是如何触发react组件得更新得

react-redux 的核心是订阅 store 变化并触发组件重新渲染。它利用 React 的 context 和 useSyncExternalStore 来高效地管理状态和 UI 更新。下面详细讲解 react-redux 是如何更新界面的。

react-redux 更新 UI 的流程

  1. 组件连接 Redux store(Provider 共享全局状态)
  2. 组件订阅 store 变化(useSelector /connect 监听数据变化)
  3. 状态改变时,触发 store.subscribe(Redux dispatch 触发 store 更新)
  4. 对比新旧状态,决定是否重新渲染(避免不必要的 UI 更新)
  5. 通知组件重新渲染(React useState 或 forceUpdate 触发渲染)

1. Redux store 如何连接到 React

在 react-redux 中,我们通过 Provider 让整个应用访问 store:

import { Provider } from "react-redux";
import { store } from "./store";export default function App() {return (<Provider store={store}><MyComponent /></Provider>);
}

Provider 使用 React Context 传递 store,子组件可以用 useSelector 访问 Redux 状态。

2. 组件如何订阅 Redux 状态

组件可以使用 useSelector 订阅 store 里的状态:

import { useSelector } from "react-redux";function MyComponent() {const count = useSelector(state => state.counter.value);return <p>Count: {count}</p>;
}

useSelector 如何监听状态变化?

  • useSelector 内部会调用 store.subscribe () 订阅 Redux store 变化
  • 当 dispatch 修改 store 时,所有 useSelector 订阅的组件都会执行
  • useSelector 会对比新旧状态(默认用 === 浅比较)
  • 如果状态没变,组件不会重新渲染,避免不必要的更新

3. Redux dispatch 如何触发 UI 更新

组件通过 dispatch 触发 Redux store 更新:

import { useDispatch } from "react-redux";
import { increment } from "./counterSlice";function MyComponent() {const dispatch = useDispatch();return <button onClick={() => dispatch(increment())}>+1</button>;
}

dispatch 更新流程

  1. dispatch (action) 触发 Redux store 更新
  2. Redux reducer 计算新 state
  3. store 触发 store.subscribe () 通知所有 useSelector 订阅的组件
  4. useSelector 比较状态,如果变化则触发组件重新渲染

4. react-redux 内部是如何订阅 store 的?

useSelector 的底层实现

在 react-redux 中,useSelector 用 useSyncExternalStore 监听 store:

import { useSyncExternalStore } from "react";function useSelector(selector) {const store = useContext(StoreContext);return useSyncExternalStore(store.subscribe,      // 订阅 Redux store() => selector(store.getState()) // 获取最新状态);
}

useSyncExternalStore 如何工作?

  • 订阅 store(store.subscribe)
  • 检测状态是否变化(通过 store.getState () 获取最新值)
  • 如果状态变了,触发组件重新渲染(React 重新执行组件)

Redux 更新 UI 的完整流程

  1. dispatch (action) 触发 store 更新

  2. reducer 计算新 state

  3. store 调用 store.subscribe () 通知组件

  4. 组件的 useSelector 重新执行,并对比状态

  5. 如果状态变化,则触发 React 重新渲染

1. 避免不必要的渲染

useSelector 只会让组件更新受影响的状态,而不是整个 store。
默认使用 === 浅比较,确保状态真的变化才会触发渲染:

const value = useSelector(state => state.value, (a, b) => a === b);

如果 useSelector 依赖对象,可以使用 reselect 进行 Memoization。

2. 使用 useCallback 和 useMemo

useDispatch () 生成的 dispatch 函数不会变,但 useSelector 可能导致组件重新渲染:

const data = useMemo(() => expensiveCalculation(state), [state]);
const handleClick = useCallback(() => dispatch(increment()), [dispatch]);

3. 代码分片(Lazy Load)

使用 redux-toolkit 的 lazyReducerEnhancer 进行动态加载 reducer,减少初始化开销。

🎯 总结

步骤React Redux UI 更新流程
1. 组件连接 storeProvider 通过 Context 提供 Redux store
2. 组件订阅状态useSelector 监听 store 变化
3. 状态变更dispatch 触发 store 更新
4. 组件重新渲染useSyncExternalStore 检测状态变更,触发 UI 更新
5. 性能优化useSelector 只更新受影响的组件,减少不必要渲染

useSyncExternalStore 是 React-Redux 中 useSelector 的底层依赖,用于安全订阅 Redux store:

React-Redux 通过 useSelector 监听 store,dispatch 触发 store 变更,useSyncExternalStore 检测 state 变化,决定是否重新渲染组件,从而实现高效的 UI 更新。

原理以及核心跟流程

记忆点:

Redux 是为解决 “复杂应用状态管理” 而生的工具,其核心是通过 “单一数据源、状态只读、纯函数修改” 的设计理念,保证状态变化的可预测性和可追踪性。

三大核心概念:Store,Action,Reducer ,2. 单向数据流

设计理念:单一数据源、状态只读、纯函数修改

解决:1. 解决 “状态共享” 问题,2. 提升状态变化的 “可追踪性”,3. 统一状态修改逻辑,4. 支持中间件扩展这些让它变得可预测性**

可追踪性: Redux DevTools 可查看每一步的 Action 和状态变化

Redux 是一个基于 Flux 架构 演变而来的状态管理库,主要用于管理 JavaScript 应用中的共享状态。它的核心目标是让应用状态变得可预测、可追踪、可调试

一、Redux 核心原理

Redux 的工作流程基于三大核心概念单向数据流

1. 三大核心概念

  • Store:全局唯一的 “状态容器”,保存整个应用的状态(一个 JavaScript 对象)。
    提供方法:getState()(获取状态)、dispatch(action)(触发状态更新)、subscribe(listener)(订阅状态变化)。

  • Action:描述 “发生了什么” 的普通 JavaScript 对象,是修改状态的 “唯一途径”。
    必须包含 type 字段(字符串,描述动作类型),例如:

    { type: 'ADD_TODO', payload: '学习 Redux' } // payload 是附加数据
    
  • Reducer纯函数,根据当前状态和 Action,计算并返回新的状态(不能直接修改原状态)。
    格式:(state, action) => newState,例如:

    javascript

    function todoReducer(state = [], action) {switch (action.type) {case 'ADD_TODO':return [...state, action.payload]; // 返回新数组(不修改原状态)default:return state;}
    }
    

2. 单向数据流

Redux 严格遵循 “单向数据流”,流程固定为:

  1. 组件通过 store.dispatch(action) 触发一个动作(例如用户点击按钮)。
  2. Store 将当前状态和 Action 传递给 Reducer。
  3. Reducer 计算并返回新的状态。
  4. Store 更新自身状态,并通知所有订阅者(组件)。
  5. 组件接收到状态变化后,重新渲染。

这个流程是 “单向” 的,状态变化的路径完全可追踪,避免了复杂应用中状态修改的混乱。

二、Redux 设计理念

Redux 的设计理念源于对 “可预测性” 的追求,核心围绕以下原则:

1. 单一数据源(Single Source of Truth)

整个应用的状态集中存储在一个 Store 中,形成一棵 “状态树”。

  • 好处:避免状态分散在多个组件或模块中,便于调试和统一管理。
  • 例如:无论多少组件需要使用用户信息,都从同一个 Store 中获取,而非每个组件维护一份。

2. 状态只读(State is Read-Only)

不能直接修改状态(state.x = y 是禁止的),必须通过 Action 描述修改意图,再由 Reducer 生成新状态。

  • 好处:确保状态变化是 “可追溯” 的(每一次变化都对应一个明确的 Action),且避免意外修改导致的 bug。

3. 用纯函数修改状态(Changes are Made with Pure Functions)

Reducer 必须是纯函数(输入相同则输出一定相同,无副作用,不修改参数)。

  • 好处:
    • 可测试性:纯函数易于编写单元测试。
    • 可预测性:相同的 state 和 action 必然产生相同的 newState。
    • 可调试性:配合 Redux DevTools 可实现 “时间旅行”(回溯任意历史状态)。

三、为什么使用 Redux?

Redux 并非所有应用都必需,但在以下场景中能显著提升开发效率:

1. 解决 “状态共享” 问题

当应用中多个组件需要共享状态(例如用户信息、购物车数据),且组件层级较深或跨组件时:

  • 传统方案(如 props 传递)会导致 “props 透传”(多层级传递),代码冗余且难维护。
  • Redux 将状态集中管理,任何组件都可直接访问或修改共享状态,无需关心组件层级。

2. 提升状态变化的 “可追踪性”

复杂应用中,状态可能被多个地方修改(例如用户操作、API 回调、定时器等):

  • 没有 Redux 时,状态变化的原因难以追溯(“为什么这个状态突然变了?”)。
  • Redux 中,每一次状态变化都对应一个 Action,配合 Redux DevTools 可清晰查看:
    • 何时触发了什么 Action?
    • 状态从什么值变成了什么值?
    • 甚至可以 “回放” 操作,快速定位 bug。

3. 统一状态修改逻辑

Redux 通过 Reducer 集中处理状态修改逻辑,避免状态修改散落在各个组件中:

  • 例如:购物车的 “添加商品”“删除商品” 逻辑都放在 cartReducer 中,而非分散在 “商品列表”“购物车页面” 等多个组件。
  • 便于团队协作(遵循统一规范)和代码维护。

4. 支持中间件扩展

Redux 可通过中间件(如 redux-thunkredux-saga)处理复杂逻辑:

  • 异步操作(如 API 请求):redux-thunk 允许 Action 返回函数,延迟 dispatch。
  • 日志记录、错误处理、状态持久化等:通过中间件统一实现,无需侵入业务代码。

总结

Redux 是为解决 “复杂应用状态管理” 而生的工具,其核心是通过 “单一数据源、状态只读、纯函数修改” 的设计理念,保证状态变化的可预测性和可追踪性。

适合场景:大型应用、多组件共享状态、需要严格追踪状态变化的场景。
不适合场景:小型应用、状态简单且无需共享的场景(此时用组件本地状态或 Context 即可)。

redux是怎样实现状态管理得

Redux 是一个基于单向数据流思想的状态管理库,其核心目标是通过严格的规则实现状态的可预测性。它的状态管理机制可以概括为:用一个全局的 “状态容器” 存储应用状态,通过 “纯函数” 修改状态,通过 “动作” 描述状态变化,并遵循严格的单向数据流规则。

一、Redux 的核心设计原则

Redux 的状态管理机制建立在三大原则之上,这些原则决定了它的实现方式:

  1. 单一数据源:整个应用的状态集中存储在一个全局的 Store 中,形成一棵 “状态树”(State Tree)。
  2. 状态只读:不能直接修改状态,必须通过触发 “动作”(Action)来间接修改。
  3. 用纯函数修改状态:状态的修改逻辑由 “减速器”(Reducer)定义,Reducer 是纯函数(输入相同则输出一定相同,无副作用)。

二、核心概念与工作流程

Redux 通过 StoreActionReducer 三个核心概念实现状态管理,三者配合形成严格的单向数据流:

1. 状态容器(Store)

Store 是 Redux 的 “核心容器”,负责:

  • 存储整个应用的状态(state);
  • 提供 getState() 方法获取当前状态;
  • 提供 dispatch(action) 方法触发状态变化;
  • 提供 subscribe(listener) 方法订阅状态变化(如通知组件重新渲染)。

创建 Store:通过 Redux 提供的 createStore 函数(或 Redux Toolkit 的 configureStore)创建,需传入一个 Reducer

import { createStore } from 'redux';
import rootReducer from './reducers';// 创建 Store,关联 Reducer
const store = createStore(rootReducer);

2. 动作(Action)

Action描述 “发生了什么” 的普通 JavaScript 对象,是修改状态的 “唯一途径”。它必须包含 type 字段(描述动作类型),可选包含 payload 字段(传递数据)。

示例

// 描述“增加计数”的动作
const incrementAction = {type: 'INCREMENT', // 动作类型(通常用常量定义)payload: 1 // 附加数据(可选)
};

触发动作需通过 store.dispatch(action)

// 触发“增加计数”的动作
store.dispatch(incrementAction);

3. 减速器(Reducer)

Reducer纯函数,负责根据 Action 计算新的状态。它接收两个参数:当前状态(state)动作(action),返回一个新的状态对象(不能修改原状态)。

工作逻辑

  • 根据 action.type 判断动作类型;
  • 基于当前状态和 action.payload 计算新状态;
  • 返回新状态(必须是全新对象,保证原状态不可变)。

示例

// 初始状态
const initialState = { count: 0 };// 计数器 Reducer
function counterReducer(state = initialState, action) {switch (action.type) {case 'INCREMENT':// 返回新状态(不修改原状态)return { ...state, count: state.count + action.payload };case 'DECREMENT':return { ...state, count: state.count - action.payload };default:// 未知动作时,返回当前状态return state;}
}

4. 完整工作流程(单向数据流)

Redux 的状态变化遵循严格的单向流程,确保可预测性:

  1. 用户交互:用户在 UI 上执行操作(如点击按钮);
  2. 触发 Action:通过 store.dispatch(action) 发送一个 Action(描述 “发生了什么”);
  3. Reducer 处理:Store 调用 Reducer,传入当前状态和 Action,Reducer 计算并返回新状态;
  4. 更新 Store:Store 用 Reducer 返回的新状态替换旧状态;
  5. 通知订阅者:Store 触发所有订阅者(如 React 组件),订阅者通过 store.getState() 获取新状态并重新渲染。

流程示意图:
UI → dispatch(Action) → Reducer → 新State → UI重新渲染

三、处理复杂场景:中间件与组合

对于复杂应用(如异步请求、日志打印等),Redux 提供了中间件(Middleware) 机制扩展功能:

1. 中间件的作用

中间件位于 dispatch(action)Reducer 之间,可对 Action 进行拦截、处理或转换。常见用途:

  • 处理异步 Action(如网络请求):redux-thunkredux-saga
  • 打印日志:redux-logger
  • 错误捕获、路由跳转等。

示例(使用 redux-thunk 处理异步)
redux-thunk 允许 Action 是一个函数(而非普通对象),用于延迟 dispatch 或条件 dispatch:

// 异步 Action 创建函数(返回函数)
const fetchData = () => {// thunk 允许返回函数,接收 dispatch 和 getState 作为参数return async (dispatch) => {dispatch({ type: 'FETCH_START' }); // 触发“开始请求”动作try {const res = await fetch('/api/data');const data = await res.json();dispatch({ type: 'FETCH_SUCCESS', payload: data }); // 成功时触发} catch (err) {dispatch({ type: 'FETCH_FAILURE', payload: err }); // 失败时触发}};
};// 触发异步 Action
store.dispatch(fetchData());

2. Reducer 组合(拆分状态)

当应用状态复杂时,可将 rootReducer 拆分为多个子 Reducer(每个负责管理一部分状态),通过 combineReducers 合并:

import { combineReducers } from 'redux';
import counterReducer from './counter';
import userReducer from './user';// 合并子 Reducer,每个子 Reducer 管理自己的状态片段
const rootReducer = combineReducers({counter: counterReducer, // 状态树中的 counter 部分由 counterReducer 管理user: userReducer // 状态树中的 user 部分由 userReducer 管理
});

四、与 React 结合:状态订阅与组件更新

Redux 本身与 React 无关,但通过 react-redux 库可实现两者无缝集成:

  1. Provider 组件将 Store 注入整个 React 应用,使所有组件可访问 Store;
  2. useSelector 钩子让组件 “订阅” 状态的特定部分(状态变化时自动重渲染);
  3. useDispatch 钩子获取 dispatch 方法,用于触发 Action。

示例

import { Provider, useSelector, useDispatch } from 'react-redux';
import store from './store';// 子组件
function Counter() {// 订阅状态中的 countconst count = useSelector(state => state.counter.count);// 获取 dispatch 方法const dispatch = useDispatch();return (<div><p>Count: {count}</p><button onClick={() => dispatch({ type: 'INCREMENT', payload: 1 })}>+</button></div>);
}// 根组件
function App() {return (<Provider store={store}> {/* 注入 Store */}<Counter /></Provider>);
}

五、核心优势:可预测性与可调试性

Redux 的状态管理机制通过以下特性保证可预测性:

  • 不可变性:状态只能通过返回新对象修改,避免意外副作用;
  • 纯函数 Reducer:相同输入一定产生相同输出,状态变化可复现;
  • 单向数据流:状态变化路径清晰,便于调试(可通过 Redux DevTools 追踪每一次状态变化的前后值、触发的 Action 等)。

总结

Redux 通过 “单一 Store 存储状态、Action 描述变化、Reducer(纯函数)计算新状态” 的机制,配合严格的单向数据流,实现了状态的可预测管理。其核心是通过约束(如状态只读、纯函数修改)减少状态管理的复杂性,尤其适合大型应用中多组件共享状态、需要严格追踪状态变化的场景。

Redux点击按钮到获取数据到数据更新发生了什么

记忆点:用户点击按钮 → 触发Action → 中间件处理异步(如请求数据)→ Reducer计算新状态 → Store更新状态 → 组件感知变化并重新渲染

在 Redux 中,从 “点击按钮” 到 “获取数据” 再到 “数据更新并反映到 UI” 是一个严格遵循单向数据流的过程,涉及多个核心概念(Action、Dispatch、Middleware、Reducer、Store、React 组件)的协同工作。以下是详细拆解:

整体流程概览

整个过程可分为 6 个关键步骤,形成闭环:
用户点击按钮 → 触发Action → 中间件处理异步(如请求数据)→ Reducer计算新状态 → Store更新状态 → 组件感知变化并重新渲染

分步详解

1. 用户点击按钮:触发交互事件

假设页面上有一个 “加载用户数据” 的按钮,点击时会触发 React 组件的事件处理函数:

// 组件中的按钮
<button onClick={handleFetchUser}>加载用户数据</button>// 事件处理函数(组件内定义)
const handleFetchUser = () => {// 这里会触发Redux的状态更新流程dispatch(fetchUserAction(123)); // 123是用户ID
};
  • 点击按钮后,handleFetchUser被调用,核心逻辑是通过dispatch方法触发一个 “获取用户数据” 的 Action。

2. 触发 Action:描述 “要做什么”

Action是一个普通 JavaScript 对象(或通过中间件支持的函数),用于描述 “发生了什么”。由于需要 “获取数据”(异步操作),这里通常使用异步 Action(需借助中间件如redux-thunk)。

示例:异步 Action 创建函数

// action.js(异步Action创建函数)
import axios from 'axios';// 1. 定义Action类型常量(便于维护)
const FETCH_USER_START = 'FETCH_USER_START'; // 开始请求
const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS'; // 请求成功
const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE'; // 请求失败// 2. 异步Action创建函数(返回一个函数,由redux-thunk处理)
export const fetchUserAction = (userId) => {// redux-thunk允许Action返回函数,接收dispatch和getState作为参数return async (dispatch) => {try {// 第一步:触发“开始请求”的Action(通知UI显示加载状态)dispatch({ type: FETCH_USER_START });// 第二步:发起异步请求(获取数据)const response = await axios.get(`/api/users/${userId}`);const userData = response.data;// 第三步:请求成功,触发“成功”的Action(携带数据)dispatch({ type: FETCH_USER_SUCCESS, payload: userData // 后端返回的用户数据});} catch (error) {// 失败时,触发“失败”的Action(携带错误信息)dispatch({ type: FETCH_USER_FAILURE, payload: error.message });}};
};
  • 这里的fetchUserAction不是直接返回 Action 对象,而是返回一个异步函数(由redux-thunk中间件处理),内部包含了 “发起请求→处理结果” 的完整逻辑。

3. 中间件处理:拦截并执行异步操作

Redux 默认只能处理同步 Action(普通对象),而 “获取数据” 是异步操作,因此需要中间件(如redux-thunk)来扩展 Redux 的功能:

  • dispatch(fetchUserAction(123))被调用时,redux-thunk会检测到 Action 是一个函数,而非普通对象,于是拦截这个函数并执行它。
  • 在这个函数内部,会先dispatch(FETCH_USER_START)通知状态 “开始加载”,再发起 API 请求,最后根据请求结果dispatch成功或失败的 Action。

4. Reducer 处理:计算新状态

Reducer 是纯函数,负责根据 Action 的类型计算新的状态。它接收当前状态(state)和 Action(action),返回一个全新的状态对象(不可修改原状态)。

示例:处理用户数据的 Reducer

// reducer.js
import { FETCH_USER_START, FETCH_USER_SUCCESS, FETCH_USER_FAILURE 
} from './action';// 初始状态(包含加载状态、数据、错误信息)
const initialState = {user: null, // 存储用户数据loading: false, // 加载状态error: null // 错误信息
};// Reducer函数
const userReducer = (state = initialState, action) => {switch (action.type) {case FETCH_USER_START:// 开始请求:更新loading为true,清空错误return { ...state, loading: true, error: null };case FETCH_USER_SUCCESS:// 请求成功:更新数据,结束加载return { ...state, loading: false, user: action.payload // 存入后端返回的数据};case FETCH_USER_FAILURE:// 请求失败:记录错误,结束加载return { ...state, loading: false, error: action.payload // 存入错误信息};default:// 未知Action:返回当前状态return state;}
};export default userReducer;
  • Reducer 根据 Action 的type字段,分别处理 “开始请求”“请求成功”“请求失败” 三种情况,返回新的状态对象(通过扩展运算符...保证原状态不可变)。

5. Store 更新:保存新状态

Redux 的Store是全局状态容器,它会接收 Reducer 返回的新状态,并替换旧状态:

  • userReducer返回新状态后,Store 会用这个新状态覆盖原来的状态(整个过程是同步的,确保状态更新可预测)。

6. 组件订阅:感知状态变化并重新渲染

React 组件通过react-redux库提供的useSelector钩子(或connect高阶组件)“订阅” Store 中的状态。当 Store 中的状态更新后:

  • useSelector会检测到它所依赖的状态片段(如state.user)发生了变化,触发组件重新渲染。
  • 组件重新渲染时,会使用新的状态数据更新 UI(如显示用户信息、隐藏加载动画、展示错误提示等)。

示例:组件订阅并展示状态

// UserComponent.jsx
import { useSelector, useDispatch } from 'react-redux';
import { fetchUserAction } from './action';const UserComponent = () => {// 订阅状态:获取user、loading、errorconst { user, loading, error } = useSelector(state => state.user);const dispatch = useDispatch();if (loading) return <div>加载中...</div>;if (error) return <div>错误:{error}</div>;return (<div><button onClick={() => dispatch(fetchUserAction(123))}>加载用户数据</button>{user && (<div><h3>{user.name}</h3><p>{user.email}</p></div>)}</div>);
};

核心总结

从点击按钮到数据更新的完整链路:

  1. 用户交互:点击按钮触发事件处理函数。
  2. 触发 Action:调用dispatch发送异步 Action(函数)。
  3. 中间件处理redux-thunk执行异步函数,发起 API 请求,并分阶段dispatch同步 Action(开始 / 成功 / 失败)。
  4. Reducer 计算:根据 Action 类型,返回新的状态对象(保证不可变性)。
  5. Store 更新:用新状态替换旧状态。
  6. 组件重渲染useSelector感知状态变化,组件重新渲染并展示新数据。

整个过程严格遵循单向数据流,且状态变化可追踪(通过 Redux DevTools 可查看每一步的 Action 和状态变化),这也是 Redux 实现 “可预测性” 的核心原因。

redux如何重置状态?

记忆点:定义一个 “重置” 类型的 Action,在 Reducer 中处理该 Action 时返回初始状态

在 Redux 中,“重置状态” 指的是将 Store 中的状态恢复到初始值(或特定的重置状态)。由于 Redux 的状态修改必须通过ActionReducer完成,因此重置状态的核心思路是:定义一个 “重置” 类型的 Action,在 Reducer 中处理该 Action 时返回初始状态

一、基础实现:手动处理重置 Action

最直接的方式是在每个 Reducer 中显式处理一个 “重置” 类型的 Action(如RESET_STATE),当该 Action 被触发时,返回初始状态。

1. 定义重置 Action

首先创建一个 “重置状态” 的 Action 类型(通常用常量定义),并创建对应的 Action 创建函数:

// actionTypes.js
export const RESET_STATE = 'RESET_STATE';// actions.js
import { RESET_STATE } from './actionTypes';// 重置状态的Action创建函数
export const resetState = () => ({type: RESET_STATE
});

2. 在 Reducer 中处理重置逻辑

每个 Reducer 都需要判断action.type是否为RESET_STATE,若是则返回自身的初始状态。

示例:单个 Reducer 的重置

// userReducer.js
import { RESET_STATE } from './actionTypes';
import { FETCH_USER_SUCCESS } from './otherActionTypes';// 初始状态
const initialState = {user: null,loading: false,error: null
};const userReducer = (state = initialState, action) => {switch (action.type) {case FETCH_USER_SUCCESS:return { ...state, user: action.payload };// 处理重置Action:返回初始状态case RESET_STATE:return initialState; default:return state;}
};export default userReducer;

3. 组合多个 Reducer 时的重置

当使用combineReducers合并多个子 Reducer 时,每个子 Reducer 都需要单独处理RESET_STATE,最终整个状态树会被重置为初始状态。

// rootReducer.js
import { combineReducers } from 'redux';
import userReducer from './userReducer';
import cartReducer from './cartReducer';// 合并子Reducer(每个子Reducer都处理了RESET_STATE)
const rootReducer = combineReducers({user: userReducer,cart: cartReducer
});export default rootReducer;

4. 触发重置

在组件中通过dispatch触发重置 Action:

import { useDispatch } from 'react-redux';
import { resetState } from './actions';const ResetButton = () => {const dispatch = useDispatch();return (<button onClick={() => dispatch(resetState())}>重置所有状态</button>);
};

二、进阶:使用高阶 Reducer 简化重置逻辑

如果应用中有多个子 Reducer,手动在每个 Reducer 中写RESET_STATE的处理逻辑会很繁琐。此时可以用高阶 Reducer(Higher-Order Reducer) 统一包装子 Reducer,自动处理重置逻辑。

1. 创建 “带重置功能” 的高阶 Reducer

// withReset.js
import { RESET_STATE } from './actionTypes';// 高阶函数:接收一个reducer和它的初始状态,返回一个新的reducer
const withReset = (reducer, initialState) => {return (state, action) => {// 若为重置Action,直接返回初始状态if (action.type === RESET_STATE) {return initialState;}// 否则调用原reducer处理return reducer(state, action);};
};export default withReset;

2. 用高阶 Reducer 包装子 Reducer

// userReducer.js
import { FETCH_USER_SUCCESS } from './otherActionTypes';
import withReset from './withReset';// 初始状态
const initialState = { user: null, loading: false };// 原reducer逻辑(无需再手动处理RESET_STATE)
const userReducer = (state = initialState, action) => {switch (action.type) {case FETCH_USER_SUCCESS:return { ...state, user: action.payload };default:return state;}
};// 用withReset包装,自动获得重置能力
export default withReset(userReducer, initialState);

通过这种方式,所有子 Reducer 只需关注自身业务逻辑,重置逻辑由高阶 Reducer 统一处理,减少重复代码。

三、使用 Redux Toolkit 简化实现

Redux Toolkit(RTK)是官方推荐的 Redux 工具集,其createSlice API 可以更简洁地实现状态重置。

1. 在 Slice 中定义 reset reducer

createSlice允许直接定义一个reset reducer,通过state = initialState重置状态(RTK 内部使用 Immer 库,支持 “直接修改” 状态的写法):

// userSlice.js
import { createSlice } from '@reduxjs/toolkit';const initialState = { user: null, loading: false };const userSlice = createSlice({name: 'user',initialState,reducers: {// 其他业务reducersetUser: (state, action) => {state.user = action.payload;},// 重置reducer:直接返回初始状态reset: () => initialState}
});// 导出reset action
export const { setUser, reset: resetUser } = userSlice.actions;
export default userSlice.reducer;

2. 全局重置:组合多个 slice 的 reset

若需要重置整个应用的状态,可以在根组件中批量触发所有 slice 的 reset action:

import { useDispatch } from 'react-redux';
import { resetUser } from './userSlice';
import { resetCart } from './cartSlice';const GlobalResetButton = () => {const dispatch = useDispatch();const handleReset = () => {// 触发所有slice的reset actiondispatch(resetUser());dispatch(resetCart());};return <button onClick={handleReset}>全局重置</button>;
};

四、特殊场景:替换 Root Reducer(不推荐)

另一种思路是通过store.replaceReducer方法替换整个 rootReducer 为初始状态的 reducer,从而实现重置。但这种方式破坏了 Redux 的单向数据流原则,且调试困难,一般不推荐。

// 初始rootReducer
import rootReducer from './rootReducer';// 存储初始reducer
const initialRootReducer = rootReducer;// 重置时替换reducer
store.replaceReducer(initialRootReducer);
// 触发一次Action使状态更新
store.dispatch({ type: 'ANY_ACTION' });

总结

重置 Redux 状态的推荐方式是:

  1. 定义一个重置类型的 Action(如RESET_STATE);
  2. 在 Reducer 中处理该 Action,返回初始状态;
  3. 对于多 Reducer 场景,使用高阶 Reducer 或 Redux Toolkit 简化逻辑。

核心原则是:始终通过 Action 触发状态变化,保持 Redux 的可预测性和可调试性

redux如何发起网咯请求?

记忆点:

在 Redux 中发起网络请求(异步操作)需要借助中间件,因为 Redux 的核心逻辑(Action、Reducer)默认只能处理同步操作。

网络请求是异步操作,需要经历 “发起请求→等待响应→处理结果(成功 / 失败)” 三个阶段。Redux 中处理异步的核心逻辑是:

  1. 通过中间件让 Redux 支持 “异步 Action”(Action 可以是函数,而非仅普通对象);

  2. 异步 Action 中包含网络请求逻辑,并在不同阶段(开始 / 成功 / 失败)dispatch 同步 Action;

  3. Reducer 根据同步 Action 更新状态(如 loadingdataerror 等)。

在 Redux 中发起网络请求(异步操作)需要借助中间件,因为 Redux 的核心逻辑(Action、Reducer)默认只能处理同步操作。最常用的方案是使用 redux-thunk(简单场景)或 redux-saga(复杂场景)。以下是具体实现方式:

核心思路

网络请求是异步操作,需要经历 “发起请求→等待响应→处理结果(成功 / 失败)” 三个阶段。Redux 中处理异步的核心逻辑是:

  1. 通过中间件让 Redux 支持 “异步 Action”(Action 可以是函数,而非仅普通对象);
  2. 异步 Action 中包含网络请求逻辑,并在不同阶段(开始 / 成功 / 失败)dispatch 同步 Action;
  3. Reducer 根据同步 Action 更新状态(如 loadingdataerror 等)。

一、使用 redux-thunk 处理网络请求(推荐入门)

redux-thunk 是 Redux 官方推荐的轻量中间件,允许 Action 返回一个函数(而非普通对象),该函数可包含异步逻辑并手动 dispatch 同步 Action。

1. 安装依赖

bash

npm install redux-thunk axios  # axios用于发起网络请求

2. 配置中间件

在创建 Store 时引入 redux-thunk 并应用:

// store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';  // 引入thunk中间件
import rootReducer from './reducers';// 应用thunk中间件
const store = createStore(rootReducer, applyMiddleware(thunk));export default store;

3. 定义 Action(包含网络请求)

异步 Action 是一个函数,内部通过 dispatch 触发 3 个同步 Action(开始请求、请求成功、请求失败),分别对应不同状态:

// actions.js
import axios from 'axios';// 1. 定义Action类型(常量,便于维护)
export const FETCH_DATA_START = 'FETCH_DATA_START'; // 开始请求
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS'; // 成功
export const FETCH_DATA_FAILURE = 'FETCH_DATA_FAILURE'; // 失败// 2. 异步Action创建函数(返回函数,由thunk处理)
export const fetchData = (url) => {// thunk允许返回一个函数,接收dispatch和getState作为参数return async (dispatch) => {try {// 第一步:发起请求前,dispatch"开始"Action(更新loading状态)dispatch({ type: FETCH_DATA_START });// 第二步:发起网络请求const response = await axios.get(url); // 例如:'https://api.example.com/data'// 第三步:请求成功,dispatch"成功"Action(携带数据)dispatch({type: FETCH_DATA_SUCCESS,payload: response.data  // 后端返回的数据});} catch (error) {// 失败时,dispatch"失败"Action(携带错误信息)dispatch({type: FETCH_DATA_FAILURE,payload: error.message || '请求失败'});}};
};

4. 编写 Reducer 处理状态

Reducer 根据 3 种同步 Action 更新状态(包含 loadingdataerror 字段,便于 UI 展示):

// reducers.js
import {FETCH_DATA_START,FETCH_DATA_SUCCESS,FETCH_DATA_FAILURE
} from './actions';// 初始状态
const initialState = {data: null,       // 存储请求结果loading: false,   // 加载状态(true表示请求中)error: null       // 错误信息(非null表示请求失败)
};// 处理请求相关的Reducer
const dataReducer = (state = initialState, action) => {switch (action.type) {case FETCH_DATA_START:// 开始请求:loading设为true,清空错误return { ...state, loading: true, error: null };case FETCH_DATA_SUCCESS:// 请求成功:存储数据,结束加载return { ...state, loading: false, data: action.payload };case FETCH_DATA_FAILURE:// 请求失败:存储错误,结束加载return { ...state, loading: false, error: action.payload };default:return state;}
};// 合并到根Reducer(如果有多个Reducer)
export default dataReducer;

5. 在组件中触发请求并展示状态

通过 useDispatch 触发异步 Action,通过 useSelector 获取状态并渲染 UI:

// DataComponent.jsx
import { useDispatch, useSelector } from 'react-redux';
import { fetchData } from './actions';const DataComponent = () => {const dispatch = useDispatch();// 从Redux状态中获取数据、加载状态、错误信息const { data, loading, error } = useSelector(state => state);// 触发网络请求的函数const handleFetch = () => {dispatch(fetchData('https://api.example.com/data'));};return (<div><button onClick={handleFetch} disabled={loading}>{loading ? '加载中...' : '获取数据'}</button>{/* 展示加载状态 */}{loading && <p>正在请求数据...</p>}{/* 展示错误信息 */}{error && <p style={{ color: 'red' }}>错误:{error}</p>}{/* 展示请求结果 */}{data && (<div><h3>请求成功</h3><pre>{JSON.stringify(data, null, 2)}</pre></div>)}</div>);
};export default DataComponent;

二、使用 redux-saga 处理复杂异步场景

对于需要取消请求、重试、防抖节流等复杂逻辑的场景,推荐使用 redux-saga。它基于 Generator 函数,通过 “声明式” 代码处理异步流程,更易测试和维护。

1. 核心步骤(简要)

  • 安装:npm install redux-saga

  • 创建 Saga:用

    takeLatest
    

    (取消重复请求)、

    call
    

    (调用异步函数)、

    put
    

    (dispatch Action)等 Effect 函数定义逻辑:

    // sagas.js
    import { takeLatest, call, put } from 'redux-saga/effects';
    import axios from 'axios';
    import {FETCH_DATA_START,FETCH_DATA_SUCCESS,FETCH_DATA_FAILURE
    } from './actions';// 异步请求逻辑
    function* fetchDataSaga(action) {try {const { url } = action.payload;const response = yield call(axios.get, url); // 调用异步函数yield put({ type: FETCH_DATA_SUCCESS, payload: response.data }); // 触发成功Action} catch (error) {yield put({ type: FETCH_DATA_FAILURE, payload: error.message }); // 触发失败Action}
    }// 监听FETCH_DATA_START Action,每次触发时执行fetchDataSaga
    export function* watchFetchData() {yield takeLatest(FETCH_DATA_START, fetchDataSaga); // takeLatest:只执行最新的请求
    }
    
  • 配置 Saga 中间件并运行:

    // store.js
    import { createStore, applyMiddleware } from 'redux';
    import createSagaMiddleware from 'redux-saga';
    import { watchFetchData } from './sagas';
    import rootReducer from './reducers';const sagaMiddleware = createSagaMiddleware();
    const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));// 运行saga
    sagaMiddleware.run(watchFetchData);export default store;
    

三、使用 Redux Toolkit 简化代码

Redux Toolkit(RTK)是官方推荐的工具集,内置了 redux-thunk,并提供 createAsyncThunk 简化异步请求逻辑:

// dataSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';// 1. 创建异步Thunk(替代手动定义异步Action)
export const fetchData = createAsyncThunk('data/fetchData', // Action类型前缀async (url, { rejectWithValue }) => {try {const response = await axios.get(url);return response.data; // 成功时返回数据(会作为payload传入fulfilled Action)} catch (error) {return rejectWithValue(error.message); // 失败时返回错误信息}}
);// 2. 创建Slice(包含Reducer)
const dataSlice = createSlice({name: 'data',initialState: { data: null, loading: false, error: null },reducers: {},// 处理异步Thunk的三种状态(pending/fulfilled/rejected)extraReducers: (builder) => {builder.addCase(fetchData.pending, (state) => {state.loading = true;state.error = null;}).addCase(fetchData.fulfilled, (state, action) => {state.loading = false;state.data = action.payload;}).addCase(fetchData.rejected, (state, action) => {state.loading = false;state.error = action.payload;});}
});export default dataSlice.reducer;

总结

Redux 发起网络请求的核心是通过中间件支持异步 Action,流程可概括为:

  1. 触发异步 Action(包含请求逻辑);
  2. 异步 Action 在不同阶段(开始 / 成功 / 失败)dispatch 同步 Action;
  3. Reducer 根据同步 Action 更新状态(loading/data/error);
  4. 组件订阅状态并展示 UI。
  • 简单场景用 redux-thunk(或 RTK 的 createAsyncThunk);

  • 复杂场景用 redux-saga(支持取消请求、流程控制等)。

为什么redux能做到局部渲染?

核心原因在于 Redux 的状态设计与 react-redux 的精准订阅机制 共同作用,实现了对组件更新范围的精确控制。

关键原理:精准订阅与状态依赖隔离,

1. Redux 的 “单一状态树” 与 “状态拆分”

2. react-redux 的 “精准订阅” 机制

3. 纯函数选择器与记忆化(Memoization)

4. 配合 React 的 memo 避免无关重渲染

为什么 Redux 比 Context API 更易实现局部渲染?

React 的 Context API 也能实现全局状态共享,但默认情况下,Context 变化会导致所有消费它的组件都重渲染(无论是否依赖变化的部分)。

这些机制共同确保:状态变化时,只有真正依赖该状态的组件才会重新渲染,从而实现高效的 “局部渲染”。

Redux 本身并不直接负责 “渲染”(渲染是 React 的工作),但 Redux 结合 React 生态(尤其是 react-redux 库)能够实现 “局部渲染”(即状态变化时,只有依赖该状态的组件重新渲染,而非整个应用)。核心原因在于 Redux 的状态设计与 react-redux 的精准订阅机制 共同作用,实现了对组件更新范围的精确控制。

关键原理:精准订阅与状态依赖隔离

1. Redux 的 “单一状态树” 与 “状态拆分”

Redux 将整个应用的状态集中在一个 “单一状态树”(store)中,但这个状态树可以被拆分为多个独立的 “状态片段”(通过 combineReducers 合并多个子 reducer)。例如:

// 状态树结构
{user: { name: 'Alice', age: 20 }, // user 状态片段cart: { items: [], total: 0 },    // cart 状态片段theme: 'light'                    // theme 状态片段
}

这种拆分使得不同组件可以只关注自己依赖的 “状态片段”,而不关心其他无关状态。

2. react-redux 的 “精准订阅” 机制

react-redux 提供了 useSelector 钩子(或 connect 高阶组件),让组件能够 只订阅自己需要的那部分状态,而非整个状态树。其核心逻辑是:

  • 状态选择器(Selector):组件通过 useSelector 传入一个 “选择器函数”,指定需要的状态片段。例如:

    // 组件只订阅 state.user 片段
    const user = useSelector(state => state.user);
    
  • 浅比较更新触发:当 Redux 状态更新时,useSelector 会对比 “当前选择的状态” 与 “上一次选择的状态”(浅比较)。如果两者不同,才会触发组件重新渲染;如果相同(或无关状态变化),则组件不会更新。

    例如:当 state.cart 变化时,只订阅 state.user 的组件不会重新渲染,因为它们的选择器结果没变。

3. 纯函数选择器与记忆化(Memoization)

为了进一步优化,react-redux 鼓励使用 纯函数选择器(输入相同则输出一定相同),并配合记忆化工具(如 reselect)缓存选择器结果,避免不必要的计算和更新:

// 使用 reselect 创建记忆化选择器
import { createSelector } from 'reselect';// 基础选择器:获取 cart 状态片段
const selectCart = state => state.cart;// 记忆化选择器:计算购物车商品总数(仅当 cart 变化时重新计算)
const selectCartItemCount = createSelector([selectCart],(cart) => cart.items.reduce((total, item) => total + item.quantity, 0)
);// 组件中使用记忆化选择器
const itemCount = useSelector(selectCartItemCount);

记忆化选择器确保:即使状态树有变化,但如果依赖的状态片段(如 cart)没变,选择器会直接返回缓存结果,避免组件不必要的重渲染。

4. 配合 React 的 memo 避免无关重渲染

当组件接收的 props 来自 Redux 状态时,即使状态变化不影响该组件,父组件的重渲染可能会 “连带” 触发子组件重渲染。此时可通过 React.memo 包装子组件,浅比较 props 变化,进一步限制渲染范围:

// 用 React.memo 包装子组件,避免 props 不变时的重渲染
const ProductItem = React.memo(({ product, onAddToCart }) => {return <div>{product.name}</div>;
});

为什么 Redux 比 Context API 更易实现局部渲染?

React 的 Context API 也能实现全局状态共享,但默认情况下,Context 变化会导致所有消费它的组件都重渲染(无论是否依赖变化的部分)。而 Redux 结合 useSelector 的精准订阅机制,本质上是对 “状态依赖” 做了更细粒度的管理:

  • Context 是 “全量更新”:Context 值变化 → 所有 useContext 组件更新。
  • Redux 是 “按需更新”:状态树部分变化 → 只有依赖该部分的组件(通过 useSelector 订阅)更新。

总结

Redux 能实现 “局部渲染” 的核心原因是:

  1. 状态拆分:将全局状态拆分为独立片段,组件只依赖自身需要的部分;
  2. 精准订阅react-reduxuseSelector 只订阅指定状态片段,通过浅比较决定是否触发更新;
  3. 记忆化优化:配合 reselect 等工具缓存选择器结果,减少无效计算;
  4. React 生态协同:与 React.memo 结合,进一步限制重渲染范围。

这些机制共同确保:状态变化时,只有真正依赖该状态的组件才会重新渲染,从而实现高效的 “局部渲染”。

react-redux
react-redux设计理念是什么,实现的原理?

记忆点:

react-redux 的设计理念是 “简化 Redux 与 React 的集成,实现状态管理与 UI 渲染的解耦,并确保高效更新”。

设计理念:

分离关注点,声明式接入,高效渲染,适配 React 生态

实现原理:Provider 组件(提供全局状态访问)和状态订阅机制(组件获取状态并响应变化)。

  1. 通过 Provider 组件利用 React Context 全局注入 Redux store,让所有组件可访问。
  2. 通过 useSelectorconnect 让组件声明式订阅状态片段,内部自动管理 store 订阅逻辑。
  3. 当状态更新时,仅触发依赖该状态的组件重渲染,通过比较机制避免无效更新。

react-redux 是连接 React 与 Redux 的官方库,其核心设计理念是 “桥接状态管理与 UI 渲染”,通过简洁的 API 让 React 组件轻松访问 Redux 状态并触发状态更新,同时解决了 “状态订阅”“渲染优化” 等关键问题。

一、核心设计理念

react-redux 的设计围绕以下几个核心目标展开,旨在解决 React 与 Redux 结合时的关键痛点:

  1. 分离关注点(Separation of Concerns)

Redux 负责 “状态管理”(状态存储、修改、流转),React 负责 “UI 渲染”(根据状态展示界面)。react-redux 作为中间层,明确划分两者的职责边界

  • 组件无需关心 Redux 的 store 如何创建、状态如何更新,只需声明 “需要什么状态” 和 “需要触发什么动作”。
  • Redux 无需关心哪些组件依赖状态,只需专注于状态的维护。
  1. 声明式接入(Declarative Access)

通过声明式 API(如 useSelectorconnect),让组件以 “描述需求” 的方式获取状态和触发更新,而非手动编写订阅逻辑。例如:

// 声明式获取状态:“我需要 state.user 这部分状态”
const user = useSelector(state => state.user);

开发者无需手动调用 store.subscribe()store.getState(),底层逻辑由 react-redux 自动处理。

  1. 高效渲染(Efficient Rendering)

核心目标之一是避免 “无关状态变化导致组件无效重渲染”。通过 “精准订阅” 和 “状态比较” 机制,确保只有依赖特定状态的组件在该状态变化时才重新渲染(即 “局部渲染”)。

  1. 适配 React 生态

深度贴合 React 的设计思想(如组件化、单向数据流、Hooks),提供两种接入方式:

  • 类组件:通过 connect 高阶组件接入。
  • 函数组件:通过 useSelectoruseDispatch 等 Hooks 接入,更符合现代 React 开发习惯。

二、实现原理

react-redux 的核心功能通过两个关键部分实现:Provider 组件(提供全局状态访问)和状态订阅机制(组件获取状态并响应变化)。

1. Provider 组件:全局注入 Redux Store

Provider 是一个 React 组件,其核心作用是通过 React Context 将 Redux 的 store 传递给所有子组件,避免 “props 透传”(通过多层组件手动传递 store)的繁琐。

实现逻辑

  • Provider 内部创建一个 React Context(React.createContext()),并将 store 作为 Context 的值。
  • 所有子组件通过消费该 Context 即可访问 store,无需手动传递。
// Provider 简化实现
const ReactReduxContext = React.createContext();function Provider({ store, children }) {return (<ReactReduxContext.Provider value={store}>{children}</ReactReduxContext.Provider>);
}

在应用入口处使用 Provider 包裹组件树,即可让所有子组件访问 store

<Provider store={store}><App />
</Provider>

2. 组件接入:获取状态与触发更新

组件通过 useSelector(Hooks API)或 connect(高阶组件 API)接入 Redux,核心是订阅 store 变化,并在状态更新时触发组件重渲染

(1)useSelector 实现原理(Hooks 方式)

useSelector 是函数组件的主要接入方式,作用是 “订阅指定状态片段,并在该片段变化时让组件重渲染”。

核心步骤

  1. 获取 store:通过 useContext(ReactReduxContext)Provider 中获取 store

  2. 计算选择器结果:执行用户传入的选择器函数(如 state => state.user),得到当前需要的状态片段(称为 selectedState)。

  3. 订阅 store 变化

    :首次渲染时,调用

    store.subscribe(listener)
    

    注册一个监听器。当

    store
    

    中的状态更新时,监听器会执行:

    • 重新计算选择器结果(nextSelectedState)。
    • 对比 selectedStatenextSelectedState(默认浅比较,可自定义比较函数)。
    • 若结果不同,调用 forceUpdate 触发组件重渲染,并更新 selectedState 为新值。
  4. 清理订阅:组件卸载时,通过 useEffect 的清理函数取消订阅(unsubscribe),避免内存泄漏。

简化代码示意

function useSelector(selector, equalityFn = shallowEqual) {const store = useContext(ReactReduxContext);const [, forceUpdate] = useReducer(x => x + 1, 0); // 用于触发重渲染// 存储当前选择的状态和上次的状态const [selectedState, setSelectedState] = useState(() => selector(store.getState()));useEffect(() => {// 定义监听器:状态变化时触发const listener = () => {const newState = store.getState();const nextSelectedState = selector(newState);// 比较新旧状态,不同则更新if (!equalityFn(selectedState, nextSelectedState)) {setSelectedState(nextSelectedState);forceUpdate(); // 触发组件重渲染}};// 订阅 store 变化const unsubscribe = store.subscribe(listener);// 初始化时检查一次(避免初始状态不一致)listener();// 组件卸载时取消订阅return () => unsubscribe();}, [store, selector, equalityFn]);return selectedState;
}

(2)connect 实现原理(高阶组件方式)

connect 是类组件的传统接入方式,本质是一个高阶组件(HOC),通过包装目标组件,将 Redux 状态和 dispatch 映射为组件的 props

核心步骤

  1. 接收映射函数:

    connect(mapStateToProps, mapDispatchToProps)
    

    接收两个函数:

    • mapStateToProps:将 Redux 状态映射为组件的 props(如 state => ({ user: state.user }))。
    • mapDispatchToProps:将 dispatch 方法映射为组件的 props(如 dispatch => ({ add: () => dispatch(addAction()) }))。
  2. 创建包装组件:

    connect
    

    返回一个高阶组件,该组件会:

    • 通过 Provider 的 Context 获取 store
    • 订阅 store 变化,当状态更新时重新计算 mapStateToProps 的结果。
    • 对比新旧 mapState 结果(浅比较),若不同则更新包装组件的 props,触发目标组件重渲染。
  3. 传递 props:将 mapStatemapDispatch 的结果合并为 props,传递给目标组件。

简化代码示意

function connect(mapStateToProps, mapDispatchToProps) {return function(WrappedComponent) {return class Connect extends React.Component {constructor(props, context) {super(props);this.store = context; // 从 Context 获取 storethis.state = {mappedProps: mapStateToProps(this.store.getState(), props)};}componentDidMount() {// 订阅 store 变化this.unsubscribe = this.store.subscribe(() => {const newMappedProps = mapStateToProps(this.store.getState(), this.props);// 对比新旧 props,不同则更新if (!shallowEqual(this.state.mappedProps, newMappedProps)) {this.setState({ mappedProps: newMappedProps });}});}componentWillUnmount() {this.unsubscribe(); // 取消订阅}render() {const dispatchProps = mapDispatchToProps(this.store.dispatch);// 合并 props 并传递给目标组件return (<WrappedComponent{...this.props}{...this.state.mappedProps}{...dispatchProps}/>);}};};
}

3. 性能优化:避免无效重渲染

react-redux 为实现 “局部渲染” 做了多层优化:

  • 精准订阅:组件只订阅自己依赖的状态片段(通过选择器函数),无关状态变化不触发更新。
  • 浅比较默认值useSelectorconnect 默认使用 “浅比较”(shallowEqual)判断状态是否变化,避免因引用变化(如返回新对象但内容相同)导致的无效更新。
  • 记忆化选择器:配合 reselect 库创建 “记忆化选择器”,缓存计算结果,避免重复计算(尤其适合复杂状态派生)。

三、总结

react-redux 的设计理念是 “简化 Redux 与 React 的集成,实现状态管理与 UI 渲染的解耦,并确保高效更新”

其实现原理可概括为:

  1. 通过 Provider 组件利用 React Context 全局注入 Redux store,让所有组件可访问。
  2. 通过 useSelectorconnect 让组件声明式订阅状态片段,内部自动管理 store 订阅逻辑。
  3. 当状态更新时,仅触发依赖该状态的组件重渲染,通过比较机制避免无效更新。

这种设计既保留了 Redux 状态管理的可预测性,又贴合 React 的组件化思想,成为 React 生态中状态管理的主流方案。

redux-thunk

记忆点:

核心设计理念是:通过允许 Action 创建器返回 “函数” 而非仅 “普通对象”,将异步逻辑(如网络请求、定时器)封装在 Action 层面,从而简化 Redux 中异步操作的处理

其核心功能是拦截并处理 “函数类型的 Action”,允许在函数内部手动控制 dispatch 的时机。其实现基于 Redux 中间件的 “洋葱模型”。

当使用 redux-thunk 时,一个异步操作的完整流程如下:

  1. 组件调用 dispatch(fetchData(url)),其中 fetchData 返回一个函数(thunk 函数);
  2. Redux 调用 redux-thunk 中间件,中间件检测到 action 是函数,于是执行该函数,并传入 dispatchgetState
  3. 函数内部执行异步逻辑(如 await api.get(url));
  4. 异步操作完成后,函数内部通过 dispatch 触发普通 Action(如 FETCH_SUCCESSFETCH_FAILURE);
  5. 这些普通 Action 再次经过中间件链,最终到达 reducer,更新状态。

redux-thunk是 Redux 生态中最基础、最常用的异步副作用处理中间件。它的设计理念简洁而实用,实现原理也非常直观。

一、设计理念

redux-thunk 的核心设计理念是:redux-thunk

redux-thunk设计理念是什么,实现的原理?

redux-thunk是 Redux 生态中最基础、最常用的异步副作用处理中间件。它的设计理念简洁而实用,实现原理也非常直观。

一、设计理念

redux-thunk 的核心设计理念是:通过允许 Action 创建器返回 “函数” 而非仅 “普通对象”,将异步逻辑(如网络请求、定时器)封装在 Action 层面,从而简化 Redux 中异步操作的处理

具体可拆解为以下几点:

  1. 兼容 Redux 核心机制,最小化侵入

Redux 核心规定:Action 必须是普通 JavaScript 对象(如 { type: 'INCREMENT' }),且状态更新必须通过纯函数 Reducer 完成。redux-thunk 不破坏这一核心机制,而是通过 “扩展 Action 类型”(允许函数作为 Action)来支持异步逻辑,保持了 Redux 的简洁性。

  1. 将异步逻辑 “本地化” 到 Action 创建器

在没有 redux-thunk 时,异步逻辑(如 “发起请求后根据结果 dispatch Action”)通常需要写在组件中,导致组件与异步逻辑耦合。redux-thunk 允许 Action 创建器返回一个函数,将异步逻辑封装在这个函数内部,使组件只需触发 Action,无需关心异步细节。

例如:

// 用 redux-thunk 封装异步逻辑
const fetchData = (url) => {// 返回函数(而非普通对象),异步逻辑写在这里return async (dispatch) => {dispatch({ type: 'FETCH_START' });try {const data = await api.get(url);dispatch({ type: 'FETCH_SUCCESS', payload: data });} catch (err) {dispatch({ type: 'FETCH_FAILURE', payload: err });}};
};
  1. 通过 “延迟 dispatch” 实现流程控制

函数类型的 Action(称为 “thunk 函数”)可以接收 dispatchgetState 作为参数,因此可以:

  • 延迟 dispatch:在异步操作完成后(如 API 响应返回)再 dispatch 结果 Action;
  • 条件 dispatch:根据当前状态(getState())决定是否 dispatch 或 dispatch 何种 Action;
  • 链式 dispatch:在一个异步流程中多次 dispatch(如 “开始→成功 / 失败” 的多步状态更新)。
  1. 轻量实用,专注解决 “简单异步场景”

redux-thunk 的设计目标是解决80% 的简单异步场景(如单次 API 请求、带条件的状态更新),而非复杂流程(如取消请求、重试)。因此它的代码量极少(核心逻辑仅几十行),学习成本低,是 Redux 异步处理的 “入门首选”。

二、实现原理

redux-thunk 作为 Redux 中间件,其核心功能是拦截并处理 “函数类型的 Action”,允许在函数内部手动控制 dispatch 的时机。其实现基于 Redux 中间件的 “洋葱模型”,具体原理如下:

1. Redux 中间件的工作机制

Redux 中间件本质是一个 “函数链”,位于 dispatch(action)reducer 之间,用于对 Action 进行拦截、修改或扩展。中间件的基本结构是一个 “三层嵌套函数”:

const middleware = store => next => action => {// 中间件逻辑:可处理action,再调用next(action)传递给下一个中间件
};
  • 第一层:接收 store(通常只用到 store.dispatchstore.getState);
  • 第二层:接收 next(下一个中间件的函数,若没有则是 reducer);
  • 第三层:接收 action(被 dispatch 的 Action),是实际处理逻辑的地方。

2. redux-thunk 的核心代码实现

redux-thunk 的核心逻辑非常简单:判断被 dispatch 的 action 是否为函数。如果是函数,则执行该函数并传入 dispatchgetState;如果是普通对象,则调用 next(action) 让其继续传递给下一个中间件或 reducer。

简化代码如下:

// redux-thunk 核心实现
function createThunkMiddleware(extraArgument) {// 中间件函数:store => next => action => {}return ({ dispatch, getState }) => next => action => {// 如果action是函数,则执行它,并传入dispatch、getState和额外参数if (typeof action === 'function') {return action(dispatch, getState, extraArgument);}// 如果是普通对象,交给下一个中间件处理return next(action);};
}// 创建默认的thunk中间件
const thunk = createThunkMiddleware();export default thunk;

3. 完整执行流程

当使用 redux-thunk 时,一个异步操作的完整流程如下:

  1. 组件调用 dispatch(fetchData(url)),其中 fetchData 返回一个函数(thunk 函数);
  2. Redux 调用 redux-thunk 中间件,中间件检测到 action 是函数,于是执行该函数,并传入 dispatchgetState
  3. 函数内部执行异步逻辑(如 await api.get(url));
  4. 异步操作完成后,函数内部通过 dispatch 触发普通 Action(如 FETCH_SUCCESSFETCH_FAILURE);
  5. 这些普通 Action 再次经过中间件链,最终到达 reducer,更新状态。

4. 扩展:传递额外参数

createThunkMiddleware 支持传入 extraArgument,允许在 thunk 函数中获取额外的上下文(如 API 客户端实例),避免硬编码依赖:

// 创建带额外参数的thunk中间件
const api = { get: (url) => fetch(url) }; // 假设的API客户端
const thunkWithExtra = createThunkMiddleware(api);// 在thunk函数中使用额外参数
const fetchData = (url) => {return async (dispatch, getState, api) => { // 第三个参数是传入的apiconst data = await api.get(url);dispatch({ type: 'FETCH_SUCCESS', payload: data });};
};

总结

redux-thunk 的设计理念是 “以最小侵入性扩展 Redux 支持异步逻辑,将异步操作封装在 Action 创建器中”,核心是允许 Action 为函数,从而手动控制 dispatch 时机。

其实现原理基于 Redux 中间件机制,通过判断 Action 类型:若为函数则执行(并传入 dispatchgetState),若为普通对象则继续传递。这种简单直接的设计使其成为处理 Redux 简单异步场景的首选工具,尽管功能有限(不支持复杂流程控制),但胜在轻量、易理解和易集成。

redux-saga
它的设计理念是什么?它有什么作用?实现的原理是怎样的?

记忆点:

设计理念是 “将副作用逻辑与组件、Action、Reducer 分离,通过声明式代码实现复杂异步流程的可控性、可测试性和可维护性"如:

1.副作用集中管理2.声明式流程定义3.可暂停与恢复的流程控制,4.可测试性优先,5.声明式流程定义

核心作用:redux-saga 主要解决 Redux 应用中复杂异步流程的管理问题,弥补了 redux-thunk 在复杂场景下的不足(如取消请求、流程复用、错误重试等)。具体作用包括:

处理复杂异步流程,取消无效操作,流程复用与组合,错误统一处理

实现原理:其核心是通过 “拦截 Action → 执行 saga 逻辑 → 调度新 Action” 的流程,结合 Generator 函数的暂停 - 恢复特性实现对异步流程的控制。

中间件注册与启动,Action 拦截与监听,Effect 解析与执行,Generator 状态管理,取消机制的实现

redux-saga 是一个用于管理 Redux 应用中副作用(Side Effects) 的中间件(注意:它专注于 Redux 生态的异步流程管理)。它的设计理念、作用和实现原理如下:

一、设计理念

redux-saga 的核心设计理念是 “将副作用逻辑与组件、Action、Reducer 分离,通过声明式代码实现复杂异步流程的可控性、可测试性和可维护性”。具体可拆解为以下几点:

  1. 副作用集中管理

“副作用” 指异步操作(如网络请求、定时器)、浏览器 API 调用(如 localStorage)等具有不确定性的操作。redux-saga 主张将所有副作用逻辑从组件、Action 创建器中抽离,统一由 “saga” 函数管理,使核心业务逻辑(如 UI 渲染、状态计算)与副作用解耦。

  1. 声明式流程定义

通过 Generator 函数Effect 描述对象 实现 “声明式” 异步流程。开发者只需描述 “要做什么”(如 “调用某个 API”“等待某个 Action”),而非 “如何做”(如手动处理 Promise 链式调用)。例如:

// 声明式描述:先调用API,再触发成功Action
function* fetchDataSaga() {const data = yield call(api.fetchData); // 声明“调用api.fetchData”yield put({ type: 'FETCH_SUCCESS', payload: data }); // 声明“触发FETCH_SUCCESS”
}

这种方式让异步流程更接近同步代码的可读性,便于理解和维护。

  1. 可暂停与恢复的流程控制

基于 Generator 函数的 “暂停 - 恢复” 特性,redux-saga 可以精细控制异步流程的执行时机。例如:

  • 等待某个 Action 触发后再继续执行;
  • 取消正在进行的异步操作(如用户多次点击按钮,取消前一次请求);
  • 实现重试、防抖、节流等复杂逻辑。
  1. 可测试性优先

saga 函数中通过 yield 产出的是 “Effect 描述对象”(而非直接执行副作用),这使得测试时无需实际发起网络请求或操作浏览器 API,只需断言 “是否产出了预期的 Effect” 即可。例如:

// 测试saga时,只需检查yield的Effect是否符合预期
const gen = fetchDataSaga();
assert.deepEqual(gen.next().value, call(api.fetchData)); // 验证是否要调用API

二、核心作用

redux-saga 主要解决 Redux 应用中复杂异步流程的管理问题,弥补了 redux-thunk 在复杂场景下的不足(如取消请求、流程复用、错误重试等)。具体作用包括:

  1. 处理复杂异步流程

对于多步骤异步操作(如 “先登录→再获取用户信息→最后加载权限”),redux-saga 可以通过 Generator 的顺序执行特性,将流程清晰串联:

function* loginFlow() {const { username, password } = yield take('LOGIN_REQUEST'); // 等待登录请求Actionconst token = yield call(api.login, username, password); // 调用登录APIyield put({ type: 'LOGIN_SUCCESS', payload: token }); // 触发登录成功yield call(api.saveToken, token); // 保存tokenconst userInfo = yield call(api.getUserInfo, token); // 获取用户信息yield put({ type: 'FETCH_USER_SUCCESS', payload: userInfo }); // 触发用户信息更新
}
  1. 取消无效操作

通过 takeLatest takeLeading 等辅助函数,可自动取消重复的异步操作。例如:用户快速点击 “加载数据” 按钮,takeLatest 会取消前一次未完成的请求,只保留最后一次:

// 监听FETCH_DATA Action,只执行最新的请求,取消之前的
function* watchFetchData() {yield takeLatest('FETCH_DATA', fetchDataSaga);
}
  1. 错误统一处理

通过 try/catchtake 监听错误 Action,集中处理异步流程中的错误,避免在每个异步操作中重复编写错误处理逻辑:

function* fetchDataSaga() {try {const data = yield call(api.fetchData);yield put({ type: 'FETCH_SUCCESS', payload: data });} catch (error) {yield put({ type: 'FETCH_FAILURE', payload: error }); // 统一触发错误Action}
}// 集中监听错误Action,进行全局处理(如弹窗提示)
function* watchErrors() {while (true) {const { payload } = yield take('FETCH_FAILURE');yield call(showErrorToast, payload); // 调用显示错误的方法}
}
  1. 流程复用与组合

saga 函数可以像普通函数一样被复用和组合,通过 fork spawn 等 Effect 实现并行或串行执行多个子流程,适合复杂业务场景的模块化拆分。

三、实现原理

redux-saga 作为 Redux 中间件,其核心是通过 “拦截 Action → 执行 saga 逻辑 → 调度新 Action” 的流程,结合 Generator 函数的暂停 - 恢复特性实现对异步流程的控制。具体原理可拆解为以下步骤:

  1. 中间件注册与启动

redux-saga 通过 createSagaMiddleware 创建中间件,在 Redux Store 初始化时被应用。初始化后,调用 sagaMiddleware.run(rootSaga) 启动根 saga(入口 saga 函数),根 saga 会启动其他子 saga(如监听特定 Action 的 saga)。

  1. Action 拦截与监听

Redux 中间件的核心作用是拦截 dispatch 的 Action。redux-saga 中间件会检查每个 Action,并将其传递给正在运行的 saga 函数(通过 take takeLatest 等 Effect 监听特定 Action)。

例如,当 saga 中使用 yield take('FETCH_DATA') 时,它会暂停等待,直到有 type: 'FETCH_DATA' 的 Action 被 dispatch,此时 saga 会恢复执行,并接收该 Action 的数据。

  1. Effect 解析与执行

saga 函数通过 yield 产出 “Effect 描述对象”(如 call(api.fetchData) put({ type: 'SUCCESS' }))。这些 Effect 是普通 JavaScript 对象,仅描述 “要执行的操作”(如 “调用某个函数”“触发某个 Action”),而非实际执行操作。

redux-saga 内部的 “调度器(scheduler)” 会解析这些 Effect 对象,并负责执行实际操作:

  • 对于 call(fn, ...args):调用 fn(...args) 并等待其返回结果(支持同步 / 异步函数)。
  • 对于 put(action):调用 store.dispatch(action) 触发新的 Action。
  • 对于 take(pattern):暂停 saga,等待匹配 pattern 的 Action 被 dispatch
  1. Generator 状态管理

由于 saga 是 Generator 函数,redux-saga 需要维护其执行状态(如当前暂停位置、已执行的步骤)。当 Effect 执行完成后(如 API 调用返回结果),调度器会将结果通过 gen.next(result) 传递给 Generator,使其从暂停处恢复执行,继续处理下一个 Effect。

  1. 取消机制的实现

redux-saga 的取消功能(如 takeLatest)基于 “任务(Task)” 概念。每个被 forkspawn 启动的 saga 都会被包装为一个 “任务”,任务可被 cancel 函数终止。取消时,调度器会向 Generator 发送 gen.throw(CancelError),使其退出执行,并清理相关资源(如正在进行的 API 请求)。

总结

redux-saga“分离副作用、声明式流程、可测试性” 为设计理念,核心作用是管理 Redux 应用中的复杂异步流程(如多步骤请求、取消操作、错误处理等)。其实现原理基于 Redux 中间件机制拦截 Action,通过 Generator 函数的暂停 - 恢复特性和 Effect 描述对象,实现对异步流程的精细控制和调度。

相比 redux-thunkredux-saga 更适合大型应用中需要复杂流程控制的场景,代价是学习成本稍高;而简单异步场景(如单次 API 请求)用 redux-thunk 更轻量。

redux-thunk 与react-saga区别优缺点适用的场景

redux-thunkredux-saga 都是 Redux 生态中用于处理副作用(尤其是异步操作) 的中间件,但二者的设计理念、功能复杂度和适用场景有显著差异。以下从核心区别优缺点适用场景三方面详细对比:

一、核心区别

维度redux-thunkredux-saga
核心设计允许 Action 创建器返回函数(而非仅普通对象),通过函数内部手动控制 dispatch 时机。基于Generator 函数Effect 描述对象,以声明式方式定义异步流程,通过中间件调度执行。
异步逻辑处理方式异步逻辑直接写在函数型 Action 中(如 async/await 配合 dispatch)。异步逻辑抽离到独立的 saga 函数中,通过 call(调用异步函数)、put(dispatch Action)等 Effect 描述操作。
流程控制能力仅支持基础流程(如顺序执行、条件判断),无内置取消、重试、并行等复杂控制。支持强大的流程控制:取消请求(takeLatest)、重试(retry)、并行 / 串行执行(fork/spawn)、防抖节流等。
代码风格类似同步代码的异步写法(async/await 为主),直观简洁。基于 Generator 的 yield 语法,流程更接近 “描述式”,需要理解 Effect 概念。
学习成本极低(理解 “函数作为 Action” 即可)。较高(需掌握 Generator、Effect、任务管理等概念)。
代码量极简(核心逻辑仅几十行)。较复杂(需定义 saga 函数、监听逻辑等)。

二、优缺点对比

1. redux-thunk

优点

  • 简单直观:学习成本几乎为零,只需理解 “Action 可以是函数”,配合 async/await 即可写出清晰的异步逻辑。
  • 轻量无依赖:核心代码仅几十行,不引入额外复杂度,对项目体积影响极小。
  • 无缝集成:与 Redux 生态完全兼容,无需额外配置,适合快速上手。
  • 适合简单场景:处理单次 API 请求、带条件的异步更新等简单逻辑时,代码简洁高效。

缺点

  • 复杂流程难以维护:多步骤异步操作(如 “登录→获取用户信息→加载权限”)会导致嵌套或链式调用,代码可读性下降。
  • 缺乏高级功能:不支持请求取消、自动重试、流程中断等复杂需求,需手动实现(易出错)。
  • 可测试性较弱:异步逻辑与 Action 创建器耦合,测试时需模拟 dispatch 和异步操作(如 jest.mock 接口),不如 saga 纯净。
  1. redux-saga

优点

  • 强大的流程控制:内置 takeLatest(取消重复请求)、race(竞争条件)、delay(延迟)等工具,轻松处理复杂异步场景。
  • 副作用集中管理:所有异步逻辑抽离到独立的 saga 文件,与组件、Action、Reducer 解耦,代码结构更清晰。
  • 极高的可测试性saga 函数通过 yield 产出 “Effect 描述对象”(如 call(api.fetch)),测试时无需实际执行异步操作,只需断言 Effect 是否符合预期。
  • 可中断与恢复:基于 Generator 的 “暂停 - 恢复” 特性,支持流程中断(如页面跳转时取消未完成的请求)。
  • 调试友好:配合 Redux DevTools 可追踪 saga 执行过程(如哪一步触发、是否完成),便于定位问题。

缺点

  • 学习曲线陡峭:需理解 Generator 函数、Effect 概念(call/put/take 等)、任务管理(fork/cancel)等,新手入门难度高。
  • 代码冗余:处理简单异步场景(如单次 API 请求)时,相比 thunk 需编写更多代码(定义 saga、监听 Action 等)。
  • 性能开销:内部需管理 Generator 状态和任务调度,对极端高频场景可能有轻微性能损耗(通常可忽略)。

三、适用场景

选择哪个中间件的核心依据是项目复杂度异步逻辑的复杂程度

1. 优先选 redux-thunk 的场景

  • 小型应用或原型开发:需求简单,异步操作少(如仅需几个 API 请求),追求开发效率。
  • 简单异步逻辑:如 “点击按钮→发起请求→更新状态” 的单次请求,无复杂流程。
  • 团队对 Generator 不熟悉:避免因学习成本影响开发进度。
  • 项目追求轻量:不希望引入额外依赖或复杂逻辑。

2. 优先选 redux-saga 的场景

  • 大型应用:存在大量复杂异步流程(如多步骤表单提交、实时数据同步、权限校验链)。
  • 需要高级流程控制:如 “用户快速点击按钮时取消前一次请求”(takeLatest)、“请求失败后自动重试”、“并行请求多个接口” 等。
  • 对可测试性要求高:需要严格测试异步逻辑,或项目采用 TDD(测试驱动开发)。
  • 副作用逻辑复用:多个组件需要共享相同的异步流程(如登录验证),saga 可作为独立模块复用。

总结

  • redux-thunk“轻量实用的入门选择”,适合简单异步场景,以最小成本解决 80% 的基础需求。
  • redux-saga“复杂场景的专业解决方案”,适合大型应用或需要精细控制的异步流程,代价是更高的学习和维护成本。

实际开发中,二者并非互斥:小型项目用 thunk 快速落地;随着业务增长,可逐步引入 saga 处理复杂逻辑(甚至二者共存,简单逻辑用 thunk,复杂逻辑用 saga)。

mobex

mobex设计思想

“MobX”(而非 “mobex”)是一个基于响应式编程的状态管理库,其设计思想围绕 ** 透明函数式响应编程(TFRP)** 展开,核心目标是让状态管理更简洁、高效且可预测。以下是其核心设计思想的详细解析:

一、核心设计哲学:自动派生(Automatic Derivation)

MobX 的核心理念是:任何可从应用状态派生的值都应自动派生。这包括 UI 渲染、计算值、副作用等。例如:

  • UI 渲染:当状态变化时,依赖该状态的 React 组件会自动重新渲染。
  • 计算值:如未完成任务数 = 总任务数 - 已完成任务数,无需手动维护,状态变化时自动更新。
  • 副作用:如网络请求、日志打印等,状态变化时自动触发。

这种设计将开发者从繁琐的手动同步逻辑中解放出来,代码更接近自然语言描述。

二、核心概念与机制

  1. 可观察状态(Observable State)

通过observablemakeAutoObservable将数据标记为可观察,MobX 会自动追踪其变化。例如

import { observable } from "mobx";const store = observable({count: 0,todos: []
});
  • 透明追踪:无需手动订阅或发布事件,MobX 通过 Proxy(或 ES5 兼容方案)自动捕获属性访问和修改。
  • 嵌套观察:对象、数组、Map/Set 等复杂结构会被深度观察,内部元素变化同样触发响应。
  1. 计算值(Computed Values)

computed定义无副作用的派生值,仅在依赖的状态变化时重新计算,并自动缓存结果。例如:

import { computed } from "mobx";class Store {@observable count = 0;@computed get doubleCount() {return this.count * 2;}
}
  • 懒加载:未被使用的计算值不会执行,避免性能浪费。
  • 纯净性:计算值必须是纯函数,确保结果可预测。
  1. 反应(Reactions)

用于处理副作用(如更新 UI、发起请求),通过autorunreactionwhen定义。例如:

import { autorun } from "mobx";autorun(() => {console.log(`当前计数:${store.count}`); // 状态变化时自动打印
});
  • 精确触发:仅依赖的状态变化时执行,避免无效操作。
  • 清理机制:组件卸载时自动清理,防止内存泄漏。
  1. 动作(Actions)

通过action标记修改状态的函数,确保状态变更的原子性和可追踪性。例如:

class Store {@observable count = 0;@action increment() {this.count++;}
}
  • 同步执行:所有动作和派生操作在同一个执行栈中同步完成,避免异步调度导致的不可预测性。
  • 事务支持:多个状态变更自动合并为原子操作,确保一致性。

三、设计原则与优势

  1. 同步性与可预测性

MobX 强制所有派生操作同步执行,避免异步调度导致的 “中间状态” 问题。例如:

store.increment();
console.log(store.doubleCount); // 立即得到更新后的值
  • 栈追踪调试:可通过调用栈直接定位状态变更的源头,调试更简单。
  1. 最小化重新计算

依赖追踪机制确保只有实际使用的状态变化时才触发更新。例如:

  • 若组件仅使用store.count,则store.todos变化不会触发该组件重渲染。
  • 未被使用的计算值自动被垃圾回收,减少内存占用。
  1. 与 React 的深度集成

通过observer高阶组件将 React 组件转换为响应式组件,自动追踪依赖的可观察状态:

import { observer } from "mobx-react-lite";const Counter = observer(({ store }) => (<div>Count: {store.count}<button onClick={() => store.increment()}>+</button></div>
));
  • 精准渲染:仅依赖的状态变化时重渲染,性能优于 Redux 的connect
  1. 轻量与低样板代码

相比 Redux,MobX 通过装饰器和自动追踪大幅减少样板代码。例如:

  • Redux 需要手动编写 action、reducer、selector 等。
  • MobX 仅需标记@observable@action,逻辑更简洁。

四、与 Redux 的对比

维度MobXRedux
核心思想响应式编程,自动派生,可变性状态函数式编程,单一数据源,不可变性状态
更新方式直接修改状态(通过 action)必须返回新状态(通过 reducer)
代码复杂度低,依赖自动追踪和装饰器高,需手动编写 action、reducer、中间件等
性能优化自动依赖追踪,精准更新需手动使用memoizereselect优化
适用场景快速开发、中小型项目、复杂交互场景大型项目、严格可追溯性、复杂异步流程

五、实践建议

  1. 合理划分状态类型:
    • 原始数据(如用户信息)用observable
    • 派生数据用computed
    • 副作用逻辑用reactionautorun
  2. 避免反模式:
    • 计算值中不允许修改状态。
    • 避免在reaction中触发其他reaction,防止链式更新。
  3. 结合工具链:
    • 使用mobx-devtools调试状态变化。
    • 配合immer简化复杂状态修改(可选)。

总结

MobX 的设计思想以透明响应式编程为核心,通过自动追踪、同步执行和精准更新,实现了状态管理的简洁性和高效性。其 “自动派生” 理念将开发者从繁琐的同步逻辑中解放出来,尤其适合需要快速迭代和复杂交互的应用。理解其核心概念(可观察状态、计算值、反应、动作)是掌握 MobX 的关键,而与 React 的深度集成进一步巩固了其在前端状态管理领域的地位。

mobX如何确保数据错误得一种处理

在 MobX 中,虽然其设计强调灵活性和开发效率,但数据错误(如意外的状态修改、数据不一致、异步操作失败等)的处理仍需通过规范的编码实践内置机制辅助工具来保障。以下是 MobX 中确保数据正确性和处理错误的核心方式:

一、通过action约束状态修改,减少意外错误

MobX 的核心原则之一是:状态修改应该集中在action(尽管不是强制的,但强烈推荐)。这一机制从根源上减少了 “意外修改状态” 导致的数据错误。

具体做法:

  1. action标记状态修改逻辑
    通过action(或makeAutoObservable自动绑定)将所有状态修改逻辑封装起来,确保状态变更可追踪,避免在任意位置随意修改状态导致的数据混乱。

    import { makeAutoObservable } from "mobx";class Store {data = null;error = null; // 专门存储数据错误信息constructor() {makeAutoObservable(this); // 自动将方法标记为action}// 用action封装数据修改,集中处理可能的错误setData = (newData) => {try {// 数据验证:提前检查数据合法性if (!newData?.id) {throw new Error("数据格式错误:缺少id字段");}this.data = newData;this.error = null; // 成功时清空错误} catch (err) {this.error = err.message; // 捕获错误并存储}};
    }
    
  2. 严格模式下强制使用action
    在开发环境中,通过configure开启enforceActions: "always",强制要求所有状态修改必须在action中进行,否则抛出错误,从编码阶段阻止不规范的状态修改。

    import { configure } from "mobx";// 开发环境配置:强制状态修改必须在action中
    configure({enforceActions: "always", // 生产环境可关闭computedRequiresReaction: true, // 防止未被观察的computed触发
    });
    

二、异步操作的错误处理

MobX 中异步操作(如 API 请求)是常见的数据错误来源(如网络失败、接口返回错误),需通过try/catch状态标记显式处理。

具体做法:

  1. action中处理异步错误
    异步操作的状态修改仍需在action中进行,同时用try/catch捕获错误,并将错误信息存储到状态中,便于 UI 展示。

    class DataStore {data = [];loading = false;error = null;constructor() {makeAutoObservable(this);}// 异步获取数据的actionfetchData = async () => {this.loading = true;this.error = null; // 重置错误try {const response = await fetch("/api/data");if (!response.ok) {throw new Error(`请求失败:${response.status}`);}const result = await response.json();this.data = result; // 成功时更新数据} catch (err) {this.error = err.message; // 存储错误信息} finally {this.loading = false; // 无论成功失败,结束加载状态}};
    }
    
  2. UI 层消费错误状态
    在组件中通过observer监听error状态,当错误发生时显示友好提示,避免错误扩散。

    import { observer } from "mobx-react-lite";const DataComponent = ({ dataStore }) => {if (dataStore.loading) return <div>加载中...</div>;if (dataStore.error) return <div className="error">错误:{dataStore.error}</div>;return <div>{/* 展示数据 */}</div>;
    };export default observer(DataComponent);
    

三、数据验证与校验

MobX 本身不提供数据验证功能,但可结合业务逻辑校验验证库(如yupzod)在状态修改前拦截错误数据。

具体做法:

action中修改状态前,对输入数据进行合法性校验,不符合要求则拒绝修改并记录错误。

import { z } from "zod"; // 引入验证库// 定义数据格式 schema
const DataSchema = z.object({id: z.number().positive(),name: z.string().min(1),
});class UserStore {user = null;error = null;constructor() {makeAutoObservable(this);}setUser = (userData) => {try {// 验证数据格式const validatedData = DataSchema.parse(userData); this.user = validatedData; // 验证通过才更新this.error = null;} catch (err) {// 捕获验证错误(zod会返回详细的错误信息)this.error = `数据验证失败:${err.errors.map(e => e.message).join(", ")}`;}};
}

四、依赖追踪与计算属性(computed)的错误处理

computed用于衍生状态(如从原始状态计算出的值),若计算逻辑出错,可能导致整个组件渲染失败。需通过错误捕获确保计算错误不影响全局。

具体做法:

computed中使用try/catch捕获计算过程中的错误,返回默认值或错误标记。

class CartStore {items = [];constructor() {makeAutoObservable(this);}// 计算总价(可能因数据错误导致计算失败)get totalPrice() {try {return this.items.reduce((sum, item) => {if (typeof item.price !== "number") {throw new Error(`商品${item.id}价格格式错误`);}return sum + item.price * item.quantity;}, 0);} catch (err) {console.error("计算总价失败:", err);return 0; // 返回默认值,避免组件崩溃}}
}

五、调试工具与状态追踪

MobX 提供了调试工具帮助定位数据错误的根源:

  1. mobx-devtools
    可在浏览器中实时查看状态变化记录、追踪action调用顺序,快速定位 “哪个 action 修改了状态”“修改前后的状态对比”,便于排查数据不一致问题。

  2. trace方法
    reactioncomputed中使用trace(),当依赖的状态变化时,会在控制台打印详细的依赖追踪日志,帮助分析 “为什么组件会重新渲染” 或 “为什么计算属性会触发”。

    import { reaction } from "mobx";reaction(() => store.data,(data) => {// 当data变化时,打印追踪日志console.trace("data changed"); }
    );
    

六、结合 React 错误边界(Error Boundary)

即使 MobX 层面处理了状态错误,组件渲染时仍可能因错误状态抛出异常。此时可使用 React 的错误边界捕获渲染错误,防止整个应用崩溃

class ErrorBoundary extends React.Component {state = { hasError: false, error: null };static getDerivedStateFromError(error) {return { hasError: true, error };}componentDidCatch(error, info) {console.error("组件渲染错误:", error, info);}render() {if (this.state.hasError) {return <div>发生错误:{this.state.error.message}</div>;}return this.props.children;}
}// 使用:包裹可能出错的组件
<ErrorBoundary><DataComponent />
</ErrorBoundary>

总结

MobX 通过以下方式确保数据错误可处理:

  1. action约束:集中状态修改,减少意外变更;
  2. 显式错误状态:存储错误信息,便于 UI 层展示;
  3. 异步与计算逻辑的try/catch:捕获操作过程中的异常;
  4. 数据验证:在状态修改前拦截非法数据;
  5. 调试工具:追踪状态变化,定位错误根源;
  6. 错误边界:防止组件渲染错误扩散。

核心思路是:将错误显式化、局部化,通过规范的流程(验证→修改→捕获→展示)确保数据错误可预测、可处理。

redux与mobx得区别与应用场景

Redux 和 MobX 是前端开发中最常用的两个状态管理库,它们在设计理念、使用方式和适用场景上有显著区别。以下从核心差异和应用场景两方面进行分析:

一、核心区别

  1. 设计思想
  • Redux:基于Flux 架构,严格遵循单向数据流函数式编程思想。
    核心原则:单一数据源(Single Source of Truth)、状态只读(State is Read-Only)、通过纯函数修改(Changes are Made with Pure Functions)。
    强调 “可预测性”,状态变化必须通过明确的流程(Action → Reducer → New State),不允许直接修改状态。
  • MobX:基于响应式编程观察者模式,更接近面向对象编程思想。
    核心原则:“任何源自应用状态的东西都应该自动获得”,允许直接修改状态,通过 “观察者” 自动追踪状态依赖并更新视图。
    强调 “简洁性”,减少模板代码,更贴近原生 JavaScript 开发习惯。
  1. 状态管理方式
  • Redux
    • 状态是不可变的(Immutable),每次修改必须返回全新的状态对象(通过Object.assign或扩展运算符实现)。
    • 状态修改流程固定:触发Action(描述 “做什么”)→ Reducer(纯函数,根据 Action 计算新状态)→ 更新Store
    • 依赖Middleware(如redux-thunkredux-saga)处理异步逻辑。
  • MobX
    • 状态是可变的(Mutable),可以直接通过赋值修改(如this.count = 1)。
    • 状态通过observable标记为 “可观察”,组件通过observer标记为 “观察者”,当被观察的状态变化时,依赖它的组件自动重新渲染。
    • 异步逻辑可直接在action中处理,无需额外中间件。
  1. 核心概念
  • Redux:概念较多且严格,包括Store(单一状态容器)、Action(普通对象,描述动作)、Reducer(纯函数,处理状态)、Dispatch(触发 Action 的方法)、Middleware(扩展 Dispatch)等。
  • MobX:概念较少且灵活,包括observable(标记可观察状态)、observer(标记依赖状态的组件)、action(标记修改状态的方法,可选但推荐)、computed(衍生状态,自动依赖追踪)等。
  1. 学习曲线与代码量
  • Redux:学习曲线较陡,需要理解不可变性、纯函数、单向数据流等概念,且模板代码较多(如定义 Action 类型、编写 Reducer)。
  • MobX:学习曲线平缓,更贴近原生 JavaScript 开发习惯,代码量少,上手快,但过度灵活可能导致团队协作时需要额外规范。
  1. 调试能力
  • Redux:由于状态不可变且变化流程可追溯,配合Redux DevTools可实现 “时间旅行”(回溯任意历史状态),调试能力极强。
  • MobX:调试相对复杂,状态变化是隐式的(直接修改),难以追踪具体修改位置,调试工具(如mobx-devtools)功能较弱。

二、应用场景

适合用Redux的场景:

  1. 大型复杂应用:如电商平台、管理系统等,状态逻辑复杂且需要严格追踪(如订单状态、用户权限)。
  2. 团队协作:多人开发时,Redux 的严格规范可保证代码风格一致,减少沟通成本。
  3. 需要可预测性和可调试性:如金融类应用,状态变化必须可追溯,便于排查问题。
  4. 需要处理复杂异步逻辑:通过redux-saga等中间件可优雅管理复杂异步流程(如并发请求、取消请求)。

适合用MobX的场景:

  1. 中小型应用:如工具类 APP、内部管理后台,状态逻辑简单,追求开发效率。
  2. 快速原型开发:MobX 代码量少、上手快,可快速实现功能验证。
  3. 状态逻辑适合面向对象管理:如游戏开发(角色属性、技能状态),用类和实例管理状态更自然。
  4. 对性能有较高要求:MobX 的响应式追踪更精确(只更新依赖状态的组件),在频繁更新的场景(如数据可视化)中性能更优。

总结

  • Redux 是 “规则驱动”,适合大型、复杂、需要强规范的应用,强调可预测性和可维护性。
  • MobX 是 “灵活驱动”,适合中小型、快速开发的应用,强调开发效率和直观性。

选择时需结合项目规模、团队熟悉度和功能需求:大型团队协作优先 Redux,小型项目或追求效率优先 MobX。

zusdand

Zustand 是一个轻量级、高性能的 React 状态管理库,以其简洁的 API、低学习成本和灵活的使用方式受到广泛欢迎。相比 Redux 等复杂的状态管理方案,Zustand 更注重 “简单直接”,无需过多样板代码,同时支持大多数现代状态管理场景(如全局状态、局部状态、异步操作、持久化等)。

一、核心设计理念

  1. 极简 API:通过简单的 “创建 store + 钩子调用” 模式管理状态,无需 Provider 包裹根组件(但支持局部 Provider)。
  2. 基于钩子:天然适配 React 钩子生态,使用 useStore 钩子直接访问状态,自动触发组件重渲染。
  3. 灵活订阅:支持精确订阅状态的部分字段,避免不必要的重渲染(性能优化)。
  4. 无侵入性:不强制修改组件结构,可渐进式集成到现有项目中。
  5. 中间件扩展:通过中间件支持持久化(persist)、开发工具(devtools)、不可变更新(immer)等功能。

二、基本用法(快速入门)

1. 安装

npm install zustand
# 或
yarn add zustand

2. 创建 Store

通过 create 函数创建一个 store,定义状态和修改状态的方法:

// stores/counterStore.js
import { create } from 'zustand';// 创建一个计数器 store
const useCounterStore = create((set) => ({// 状态count: 0,// 修改状态的方法(通过 set 函数更新)increment: () => set((state) => ({ count: state.count + 1 })),decrement: () => set((state) => ({ count: state.count - 1 })),reset: () => set({ count: 0 }), // 直接设置新状态
}));export default useCounterStore;

3. 在组件中使用

通过 useStore 钩子访问状态和方法,组件会自动响应状态变化:

// CounterComponent.jsx
import useCounterStore from './stores/counterStore';function Counter() {// 访问状态和方法(按需获取,避免全量订阅)const count = useCounterStore((state) => state.count);const { increment, decrement, reset } = useCounterStore();return (<div><p>Count: {count}</p><button onClick={increment}>+</button><button onClick={decrement}>-</button><button onClick={reset}>Reset</button></div>);
}

三、核心特性与高级用法

1. 选择性订阅(性能优化)

默认情况下,组件会在 store 中任何状态变化时重渲染。通过 “精确选择状态字段” 可避免无效重渲染:

// 只订阅 count 字段,其他状态变化不会触发重渲染
const count = useCounterStore((state) => state.count);// 错误示例:每次渲染会创建新函数,导致不必要的重渲染
const { count, increment } = useCounterStore((state) => ({ count: state.count, increment: state.increment 
}));// 正确示例:使用 shallow 比较(需引入 shallow)
import { shallow } from 'zustand/shallow';
const { count, increment } = useCounterStore((state) => ({ count: state.count, increment: state.increment }),shallow // 浅层比较,避免因对象引用变化触发重渲染
);

2. 异步操作

支持在 store 中直接处理异步逻辑(如 API 请求):

// stores/userStore.js
import { create } from 'zustand';const useUserStore = create((set) => ({user: null,isLoading: false,error: null,// 异步获取用户信息fetchUser: async (userId) => {set({ isLoading: true, error: null });try {const response = await fetch(`/api/users/${userId}`);const data = await response.json();set({ user: data, isLoading: false });} catch (err) {set({ error: err.message, isLoading: false });}},
}));

组件中使用:

function UserProfile({ userId }) {const { user, isLoading, error, fetchUser } = useUserStore();useEffect(() => {fetchUser(userId);}, [userId, fetchUser]);if (isLoading) return <div>Loading...</div>;if (error) return <div>Error: {error}</div>;return user ? <div>Name: {user.name}</div> : null;
}

3. 中间件(Middleware)

Zustand 支持通过中间件扩展功能,常用中间件包括:

  • persist:持久化状态(localStorage/sessionStorage)

    javascript

    import { create } from 'zustand';
    import { persist } from 'zustand/middleware';const useCartStore = create(persist((set) => ({items: [],addItem: (item) => set((state) => ({ items: [...state.items, item] })),}),{name: 'cart-storage', // localStorage 的 keygetStorage: () => localStorage, // 可选:默认 localStorage,可改为 sessionStorage})
    );
    
  • devtools:集成 Redux DevTools 调试

    javascript

    import { devtools } from 'zustand/middleware';const useStore = create(devtools((set) => ({// ...状态和方法}))
    );
    
  • immer:使用 immer 简化不可变更新(允许 “直接修改” 状态)

    import { immer } from 'zustand/middleware';const useTodoStore = create(immer((set) => ({todos: [],addTodo: (text) => set((state) => {// 直接“修改”数组(immer 会转换为不可变更新)state.todos.push({ id: Date.now(), text, done: false });}),}))
    );
    

4. 多 Store 与状态组合

Zustand 推荐按领域拆分多个 store(如 userStorecartStore),避免单一 store 过大:

// 分别创建多个 store
const useUserStore = create(/* ... */);
const useCartStore = create(/* ... */);// 组件中按需使用多个 store
function Checkout() {const user = useUserStore((s) => s.user);const cartItems = useCartStore((s) => s.items);// ...
}

如需组合多个 store 的状态,可手动合并或使用 combine 工具:

import { create, combine } from 'zustand';const useCombinedStore = create(combine({ count: 0, user: null }, // 初始状态(set) => ({increment: () => set((s) => ({ count: s.count + 1 })),setUser: (user) => set({ user }),}))
);

四、与其他状态管理方案的对比

特性ZustandReduxContext + useReducer
代码量极少(无样板代码)多(action、reducer 等)中等(需 Provider 嵌套)
学习成本高(概念多)中(依赖 React 知识)
性能高(精确订阅)中(需手动优化)低(容易过度渲染)
中间件支持丰富(内置常用功能)丰富(生态成熟)无(需手动实现)
适用场景中小型应用、快速开发大型复杂应用简单局部状态

五、适用场景

  1. 中小型 React 应用:无需复杂配置即可实现全局状态管理。
  2. 需要性能优化的场景:精确订阅机制避免不必要的重渲染。
  3. 快速原型开发:极简 API 加速开发流程。
  4. 替代 Context + useReducer:在状态复杂但未达到需要 Redux 的程度时使用。

总结

Zustand 以 “简洁、灵活、高性能” 为核心优势,通过钩子化的 API 降低了状态管理的门槛,同时支持大多数现代应用的需求(异步、持久化、调试等)。对于希望摆脱 Redux 繁琐配置,或需要优化 Context 性能问题的项目,Zustand 是一个极佳的选择。

相关内容

写下自己求职记录也给正在求职得一些建议-CSDN博客

从初中级如何迈入中高级-其实技术只是“入门卷”-CSDN博客

前端梳理体系从常问问题去完善-基础篇(html,css,js,ts)-CSDN博客

前端梳理体系从常问问题去完善-工程篇(webpack,vite)_前端系统梳理-CSDN博客

http://www.dtcms.com/a/456730.html

相关文章:

  • 基于单片机的双档输出数字直流电压源设计
  • FastDDS
  • leetcode LCR.衣橱整理
  • 基于单片机的自动存包柜设计
  • 竞价关键词排名软件保山网站建设优化
  • 电力市场学习笔记(1):什么是电力现货交易
  • 单例模式:原理、实现与演进
  • 用AI帮忙,开发刷题小程序:微信小程序中实现Markdown图片解析与渲染功能详解
  • 天津魔方网站建设WordPress模板转换typecho
  • 小工具大体验:rlwrap加持下的Oracle/MySQL/SQL Server命令行交互
  • AI智能体的未来:从语言泛化到交互革命
  • 云计算划分标准与Kubernetes NetworkPolicy深度解析
  • 学院网站建设功能网络公关案例
  • 【HTML】实现一个AI角色切换网页页面
  • 【51单片机】【protues仿真】基于51单片机矩阵电子琴系统
  • 网站怎样做免费优化有效果组织部信息化建设官方网站
  • 使用telnet进行Dubbo接口测试
  • 定时器实现非阻塞式程序
  • ArrayList - 数据结构 - 数组
  • 做网站的注意什么问题哪些经营范围是包含网站开发的
  • 【Python】基于 PyQt6 和 Conda 的 PyInstaller 打包工具
  • MyBatis Plus 核心功能与用法
  • LNMP架构实践
  • 自己怎么建个网站赚钱吗外贸品牌推广公司
  • 在线咨询 1 网站宣传建立免费公司网站
  • 10-存储过程和存储函数
  • leetCode101:对称二叉树
  • 【Linux】网络部分——Socket编程 UDP实现网络云服务器与本地虚拟机的基本通信
  • 实战项目:鸿蒙多端协同智能家居控制 App 开发全流程
  • 个人用云计算学习笔记 --19 (MariaDB服务器)