Skip to content

FLUX思想

单向数据流

  • Dispatcher(分发器)
  • Store(存储)
  • Action(行为)
  • View(视图) 其中,视图层用来展示 Store 的数据,可以使用任何框架例如 React,当视图层使用 Store 的时候也需要完成对它的订阅,这样当状态发生变化时 Store 可以通知 View 来更新页面。用户可以在 View 进行交互进而派发出 Action。

各种状态管理库的对比

原生React

  • useEffect + useState 实现状态管理

缺点是: 缺乏一些高级特性

  • 数据分页
  • 数据流向

虚拟dom diff算法:

  • 从根组件开始逐层递归比较,不会跨层级比较相似节点;出现不同会整个替换(整个销毁,整个重建);
jsx
// 旧结构
<div>
  <ComponentA />
</div>

// 新结构 → ComponentA 会被卸载后重新挂载
<section>
  <div>
    <ComponentA />
  </div>
</section>
  • key优化
  • 类型不同直接替换

使用zustand 或者 jotai

基于 selector手动进行状态更新:(React Redux、Zustand、XState)

jsx
const usePersonStore = create((set) => ({
  firstName: '',
  lastName: '',
  updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
  updateLastName: (lastName) => set(() => ({ lastName: lastName })),
}))

function App() {
  const { firstName } = usePersonStore()
  return <div>firstName: {firstName}</div>
}

这样取值会导致重新渲染,需要手动优化

js
function App() {
  const firstName = usePersonStore(state => state.firstName)
  return <div>firstName: {firstName}</div>
}

基于 原子之间的交错组合,

每一个原子只管理自己的小片段,通过原子直接的依赖关系(组合与派生)来阻止re-render;

js
const firstNameAtom = atom('')

const lastNameAtom = atom('')

const fullNameAtom = atom(get => {
  const firstName = get(firstNameAtom)
  const lastName = get(lastNameAtom)
  return firstName + lastName
})

function App() {
  const firstName = useAtomValue(firstNameAtom)
  return <div>firstName: {firstName}</div>
}

zustand特性:

适合用于中大项目中,需要大量更新的地方; 0. 基于订阅发布者模式,实现了类似 "Signal" 的细粒度响应式更新;

  1. 将依赖存储在外部,绕过React的生命周期机制,绕过React的虚拟dom diff机制;
  2. 不触发父组件和子组件的渲染流程,独立于React的树形更新机制; 使用use-sync-external-store进行同步更新, 并且使得父子组件之间,兄弟组件之间不会相互影响;

这个api的作用:

  • 解决“撕裂问题”
  • 让React组件可以安全的订阅外部状态,使用外部数据源
  • 实现精确更新,只有自己的状态变化时才会更新,父子组件之间,兄弟组件之间不会相互影响
js
// React Context 的典型更新流程
<Context.Provider value={state}>
  <ChildA />  // 即使只用到 value.a , 但是所有 消费组件 都会重新渲染
  <ChildB />  // 即使只用到 value.b
</Context.Provider>

zustand优化之后:

js
// Zustand 订阅模式
const ChildA = () => {
  const a = useStore(state => state.a) // 仅订阅 a
  return <div>{a}</div>
}

const ChildB = () => {
  const b = useStore(state => state.b) // 仅订阅 b
  return <div>{b}</div>
}

当 state.a 变化时,只有 ChildA 重新渲染

当 state.b 变化时,只有 ChildB 重新渲染

  1. 流程对比:
md
传统 React 更新流程:
[State Change] 
  → [触发组件重新渲染] 
  → [Virtual DOM Diff] 
  → [DOM 更新]

Zustand 更新流程:
[State Change]
  → [执行所有选择器]  (选择器基于订阅发布者模式, 更新时会遍历所有选择器)
  → [浅比较过滤变化项] 
  → [仅触发需更新的组件] 
  → [直接 DOM 更新]
  1. 关键设计约束: 不可变更新、保持useStore稳定
js
// 错误示例(直接修改)
setState(state => {
  state.user.name = 'Bob' // 不会触发更新!
  return state
})

// 正确做法(不可变更新)
setState(state => ({
  ...state,
  user: { ...state.user, name: 'Bob' }
}))


// 错误示例(每次渲染新函数)
const user = useStore(state => ({ name: state.name, age: state.age }))

// 正确做法(使用记忆化选择器)
const selectUser = useCallback(state => ({ name: state.name, age: state.age }), []);
const user = useStore(selectUser)
  1. 浅比较短路原则,仅仅做第一层比较;而且在流程的第一步就比较,如果相同的话,就跳过整个流程; 如果是原生React的话,要生成虚拟dom树,在dom diff中才会比较;
js
// React 组件更新过程
function Component() {
  const [state, setState] = useState({ count: 0 })
  
  // 即使新值相同也会触发以下流程:
  // 1. 执行组件函数
  // 2. 生成新 Virtual DOM
  // 3. Diff 算法比对
  // 4. 决定是否更新真实 DOM
  return <Child />
}
js
function ConnectedComponent() {
  const slice = useStore(state => state.slice)
  
  // 更新判断前置:
  // 1. 状态变化时立即执行浅比较
  // 2. 不同 → 触发组件重渲染
  // 3. 相同 → 跳过整个渲染周期
  return <Child />
}

需要深比较的地方特殊处理

js
// 自定义比较器示例
const useDeepStore = create((set) => ({...}))

// 在组件中使用深比较
const data = useStore(
  state => state.deepData,
  (oldVal, newVal) => _.isEqual(oldVal, newVal) // lodash 深比较
)

设计哲学

  • 提前短路原则: 在更新流程的最早期就触发更新,提高效率;
  • 订阅发布者模式, 类似 "Signal" 的细粒度响应式更新
  • 最小化重渲染原则:仅仅在切片更新的时候触发更新; 与原生的Context API的层连更新形成对比;
  • 引用透明性: 任何状态变更必须通过返回一个全新的对象(或不可变数据结构)来触发更新; 在不引入immmer的情况下,不能直接更改对象;

以此来避免深度递归;

特性:

  • 细粒度的依赖追踪,无需手动声明依赖; 在内部会维护一个依赖图, 底层通过全局Store维护状态,配置监听器;
  • 直接通过原子依赖同样的跳过虚拟dom,直接进行最小粒度的更新;
  • 支持异步,(zustand要手动处理, context要结合外部库比如swr)
  • 作用域隔离,通过ProviderScope隔离原子状态,适合微前端以及同一页面渲染多个独立组件的情况

Jotai

组合派生,依赖追踪自动更新

优势:

  • 避免父组件更新的时候,所以子组件都触发渲染,即使它没有依赖变化的状态,也没有改变;
  • 可以避免层级依赖,一层一层传递;比如下文中,组件b和c的状态依赖于a,但是他们无需作为a的子组件来传props;

天然支持跨组件通信,组合派生状态的使用场景;

JS
// 组件 A 中定义原子
const countAtom = atom(0);
const CounterA = () => {
  const [count, setCount] = useAtom(countAtom);
  return <button onClick={() => setCount(count + 1)}>A: {count}</button>;
};


// 组件 B 派生状态,组件 B 可以直接引用 countAtom,并派生出双倍值:
const doubledAtom = atom((get) => get(countAtom) * 2);
const DisplayB = () => {
  const [doubled] = useAtom(doubledAtom);
  return <div>B: 双倍值 = {doubled}</div>;
};


// 组件 C 进一步派生, 组件 C 可以进一步基于 doubledAtom 生成平方值:
const squaredAtom = atom((get) => get(doubledAtom) ** 2);
const DisplayC = () => {
  const [squared] = useAtom(squaredAtom);
  return <div>C: 平方值 = {squared}</div>;
};

对比传统:

js
// 父组件维护状态,通过 Context 传递
const App = () => {
  const [count, setCount] = useState(0);
  return (
    <CountContext.Provider value={{ count, setCount }}>
      <CounterA />  // 子组件触发更新
      <DisplayB />  // 孙组件显示双倍值
      <DisplayC />  // 曾孙组件显示平方值
    </CountContext.Provider>
  );
};

// 子组件需逐层获取 Context
const DisplayB = () => {
  const { count } = useContext(CountContext);
  const doubled = count * 2; // 每次父组件更新都会触发重渲染
  return <div>B: {doubled}</div>;
};

实现原理

https://www.paradeto.com/2023/10/30/jotai/

也是基于订阅发布者模式,用类似“信号”机制的方式,

也是跳过虚拟dom的批量diff的,通过原子依赖的精准更新;

请求状态 React Query,SWR

比如使用useRequest, 拥有分页,缓存错误处理,防抖节流,分页等等功能

本站访客数 人次 本站总访问量