React数据流管理深入对比与剖析
文章内容较长,读者可以配合目录和小结阅读。(文末附上全文内容思维导图,按需取用。思维导图真是整理学习的好工具!夸夸)
React Hooks革新了状态管理,但基于Hooks的数据流方案选择太多。本文将深度剖析主流React数据流方案,旨在深入对比澄清其应用场景、设计哲学、优缺点,助你迅速梳理对React数据流管理的认识,锁定最适合的解决方案。
1. 简单Hook(useState+useEffect)
适用场景: 局部状态管理组件内简单的数据管理
优势: 使用简单、没有负担
劣势: 不擅长跨组件、复杂的数据管理
2. useReducer
1. 适用场景:
局部状态管理, 单组件内有复杂的逻辑处理。
有一种说法是,useReducer是更高级的useState。useReducer可以处理一组件内复杂的数据逻辑,比如数据更新合并
2. 优势
-
1. 支持组件内复杂逻辑处理,可以让数据更新合并
- 组件内部多个变量,且变量之间有相互依赖/联动逻辑(useState就做不到)
-
2. React内置Hook,无需安装依赖
-
3. 与useContext结合,使得状态和状态更新都可以在组件树中共享
3. 劣势
- 不擅长全局复杂状态管理
4. 场景举例 useReducer VS useState
举一个业务场景例子:
假设我们有一个购物车应用,其中包含多个商品项目,每个项目有数量、单价、是否选中等状态。用户可以增加/减少数量、选择/取消选择商品、删除商品等。
- 使用useState
const [cartItems, setCartItems] = useState([]);
const [selectedItems, setSelectedItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
// 其他状态...
当涉及到一个操作需要改变多个状态时(例如,删除一个商品需要更新 cartItems, selectedItems 和 totalPrice),代码会变得难以管理和维护。
- 使useReducer
使用 useReducer,你可以将所有购物车相关的逻辑放在一个地方:
const initialState = {
cartItems: [],
selectedItems: [],
totalPrice: 0,
};
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
// 逻辑...
return newState;
case 'REMOVE_ITEM':
// 逻辑...
return newState;
case 'TOGGLE_ITEM_SELECTION':
// 逻辑...
return newState;
case 'UPDATE_TOTAL_PRICE':
// 逻辑...
return newState;
default:
return state;
}
}
const [state, dispatch] = useReducer(cartReducer, initialState);
5. 设计思想
- useState + reducer(函数式、发布订阅)
PS: reducer理解 (纯函数)
- Reducer 是一个纯函数,用于执行状态转换。它接收当前状态和一个 action(动作),然后返回一个新状态。
- Reducer的特点其实就是纯函数,关于这个模式的理解见[[##8.补充知识点]]
3. useContext
1. 适用场景:简单场景的全局状态管理。组件间共享数据流
2. 优势: 1. 使用简单
3. 劣势:
-
1. 需要在最外层用provider包裹组件,与UI不解耦
- hack解决方案:unstated-next
-
2. 更新粒度太粗,同上下文中任何值的改变都会导致冲渲染
- (PS: hack解决方案use-context-selector)
4. Redux
1) 使用场景:
- 复杂度较高时,团队规模较大
- 要需要状态持久化或可回溯的场景
2) 核心竞争力
-
1. 状态持久化(组件销毁也可保留状态)
-
2. 状态可回溯(可以基于action打log可以回溯)
- 比如redux-dev-tools有“时光机”功能
-
3. 函数式编程 (纯函数,无副作用)
-
4. 中间件支持 (针对异步数据流,社区提供了很多中间件解决方案)
- redux middleware 将 action 对接到 reducer 的黑盒的控制权暴露给了开发者
3) 缺点
-
1. 要写的代码多、复杂
-
2. 只有一个全局store,多组件可能出现store状态残留
- 多组件共用store里状态要注意初始化清空问题
-
3. 无脑发布订阅
-
每次dispatch都会遍历所有reducer,更新粒度太粗,渲染浪费
- 可以搭配useSelector(默认浅比较,可以提供自定义函数)解决
-
-
4. 交互频繁时会有卡顿
-
5. 需要搭配其他库一起使用
关于store状态残留具体是什么场景?
Redux 设计之初就是为了提供一个全局的、单一的状态管理容器(Store)。这种设计使得状态管理变得集中但也相对复杂。因为所有的状态都存储在一个全局对象中,所以很容易出现状态残留的问题,特别是在以下几种情况:
- 单页应用:在单页应用(SPA)中,页面间的状态切换可能不会触发页面重新加载,导致上一个页面的状态残留。
- 组件的动态加载与卸载:当一个组件卸载时,它在 Redux Store 中的状态不会自动清除。如果下次该组件重新加载,它可能会读取到旧的、已经不再相关的状态。
- 异步操作:异步操作(如 API 请求)可能会在完成后改变状态,如果没有正确处理,可能会导致状态残留。
例如,假设你有一个购物车页面和一个产品列表页面。在购物车页面,你可能会将一个 isCartVisible 的状态设置为 true。当你导航到产品列表页面时,除非你显式地将 isCartVisible 设置回 false,否则这个状态就会“残留”。
代码示例
假设在购物车组件中有如下逻辑:
useEffect(() => {
dispatch({ type: 'SHOW_CART' }); // 设置 isCartVisible 为 true
return () => {
dispatch({ type: 'HIDE_CART' }); // 设置 isCartVisible 为 false
};
}, []);
在这个例子中,useEffect 的清理函数会在组件卸载时运行,将 isCartVisible 设置回 false,从而避免状态残留。
5. 设计思想
- 函数式库。发布订阅者模式
5. Mobx
Mobx把最简单的操作提供给了用户,其他自己内部实现,主打的响应式设计让我们只关注和操作Observable data
1. 优势
-
1. 代码量少
- 基于Observable,自动订阅、自动发布,去dispatch
-
2. 基于数据劫持。更新粒度细且精准,无需SCU
-
redux不能直接修改state,mobx可随意修改
-
redux需要对监听的组件走SCU优化,减少重复render。mobx是智能的。
-
-
3. 多store抽离业务逻辑(Model View分离)
- redux只有一个store,mobx多store,可以根据功能或业务逻辑将状态分解到不同的 Store 中。
-
4. 响应式良好(频繁的交互可以胜任)
2. 缺点
-
1. 没有状态回溯能力 (直接修改对象引用,很难做状态回溯)
-
2. 没有很好的异步流解决方案、没有中间件能力
- mobx、redux都不能很好处理异步数据流,但redux提供了applyMiddleware,而mobx无
-
3.多store维护成本高
- store数增多,维护成本会增加,多store之间数据共享容易出错
-
- 副作用
3. 设计思想
- 响应式库,观察者模式(数据劫持Proxy)
4. 使用场景
- 复杂度一般时,小规模团队或开发周期较短、要求快速上线时,使用Mobx
6. Zustand
1. 优势
-
1. 简单易用
-
2. 性能优秀,只在需要时重新渲染组件
-
3. 与React hooks完美集成,基于Hooks构建,使用非常自然
2. 缺点
-
1. 缺乏中间件支持
-
2. 社区不够成熟
-
3. 对于非常大和复杂的项目可能不是最佳选择
3. 设计思想
- 函数式编程+Hook钩子思想
7. rxjs
1. 优势
-
1. 强大的异步数据流处理,redux、mobx等都不擅长
-
2. 纯函数
- 在数据流动过程中,不会改变已有Observable(可观察对象),会返回一个新的Observable
-
3. 有强大的操作符支持异步处理,又称 lodash for async
-
4. 独立
- 不依赖于任何框架(不依赖React,基于js),可以任意搭配
2. 缺点
-
1. 学习曲线陡险
-
2. 事件流高度抽象
3. 设计思想
RxJS把一切抽象成流,提供了非常多且强大的操作符,且无副作用,无论是处理同步还是异步数据流都游刃有余
-
基于Event Stream,把前端的一切转化为数据源后,
-
函数式+响应式。观察者模式 + 迭代器模式
4. 总结
使用场景:
- 复杂度较高,且数据流(尤其是异步数据)混杂时,使用RxJS
8. 关于不同数据流管理方式的设计模式的说明
梳理下来,数据流的管理方式主要是用了以下三种设计模式
1. 函数式编程
-
reducer+ action 模式
- 1. 纯函数与不可变性
- 重点:没有副作用,提供可预测性和易于测试的状态管理。
- 纯函数:输出完全由输入决定,没有副作用。
- 数据不可变:状态不被直接修改,而是生成新的状态。
- 重点:没有副作用,提供可预测性和易于测试的状态管理。
- 2. 高阶函数 (connect)
- 重点:扩展功能,复用逻辑。
- 如 Redux 的 connect(),用于组件和状态库的连接。
- 重点:扩展功能,复用逻辑。
- 1. 纯函数与不可变性
-
rxjs模式
- 将副作用转化为数据源,将副作用隔离在管道流处理之外
2.响应式编程
- 数据劫持与观察者模式
- 特点:实时、精准地响应状态变化。
- 如 MobX 使用 Proxy 进行数据劫持,做到细粒度自动更新视图。
3. 迭代器模式
RxJS 与迭代器模式
Rxjs通过迭代器模式与发布订阅模式结合,提升对异步数据的精准性与可控性。
- Observer(订阅者) 接口:定义了 next、error 和 complete 方法。
var observer = {
next: val => {...},
error: err => {...},
complete: () => {...}
};
observer.next()
- 事件流(Observable发布者): 通过Observales创建事件流,可以发出三种类型通知 next\error\complete
const observable = new Observable(observer => {
observer.next('value1');
observer.next('value2');
observer.complete();
});
-
通过 Observer 接口,每个 Observable 对象可以关联多个 Observer。这些 Observer 通过迭代器模式的 next、error、complete 方法响应 Observable 的状态变化。
-
通过这种结合,RxJS 可以对复杂的异步操作流程进行抽象和简化,从而更容易地处理和控制异步数据。
全文思维导图笔记
小结
在这里,对于主流React数据管理方案也介绍完了,从最基础的useState和useEffect,到更加复杂和强大的RxJS模式,各有各的优势和适用场景。本质上,这些数据管理方案都是不同设计模式的体现,而最合适的选择则依赖于具体的业务需求和场景。小结一下场景选择
- 局部组件内简单状态管理 -- useState + useEffect
- 局部组件内复杂状态管理,如多个相互依赖的状态 -- useReducer
- 跨组件状态管理(复杂度一般) —— useReducer + useContext
- 大型应用全局状态管理(复杂度一般)—— zustand/mobx。
- 大型应用全局状态管理(项目复杂、需要易回溯易测试) —— redux
- 复杂混乱的状态(设计拖拽等混乱复杂数据流) -- rxjs
转载自:https://juejin.cn/post/7273026570941562919