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" 的细粒度响应式更新;
- 将依赖存储在外部,绕过React的生命周期机制,绕过React的虚拟dom diff机制;
- 不触发父组件和子组件的渲染流程,独立于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 重新渲染
- 流程对比:
md
传统 React 更新流程:
[State Change]
→ [触发组件重新渲染]
→ [Virtual DOM Diff]
→ [DOM 更新]
Zustand 更新流程:
[State Change]
→ [执行所有选择器] (选择器基于订阅发布者模式, 更新时会遍历所有选择器)
→ [浅比较过滤变化项]
→ [仅触发需更新的组件]
→ [直接 DOM 更新]
- 关键设计约束: 不可变更新、保持
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)
- 浅比较短路原则,仅仅做第一层比较;而且在流程的第一步就比较,如果相同的话,就跳过整个流程; 如果是原生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)
- 作用域隔离,通过
Provider
和Scope
隔离原子状态,适合微前端以及同一页面渲染多个独立组件的情况
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, 拥有分页,缓存错误处理,防抖节流,分页等等功能