对使用Redux和Redux-saga管理状态的思考
原文地址在我的博客, 转载请注明出处,谢谢!
概述
本文介绍了我对 Redux 状态管理的思想、原理、架构方法的认识和思考以及配合redux-saga处理异步操作的实践
前言
You know, React 只是属于MV*架构模式的 view 层,是一种状态机,只使用 React 难以控制大型、复杂的应用,它需要一些框架来帮助管理状态,因此如何有效、简单、易于测试地管理这个状态机是各种架构框架感兴趣的。Facebook 早就意识到这个问题并提出了 Flux 架构,比较复杂; 后来出现了 Redux、Mobx 等。MobX 可以处理简单数据流的场景,可以实现精确更新; Redux 是从 Flux 和其他框架借鉴了一些思想, 它比 Flux 简单、易于理解、用于处理复杂数据流,并具有很强的扩展性,社区诞生了像redux-thunk、redux-promise、redux-saga等中间件用于方便地处理异步操作。最近也在项目中使用了 Redux 及其中间件 redux-saga 来管理状态和处理异步操作,这篇文章就来谈谈我对它们的思考和实践。
正文
Redux 思想
先来谈谈背景(需求)
我觉得理解Redux的思想,谈谈MV*架构模式的思路也许会有帮助。
MV*架构模式,它们的核心都是职责分离、解耦,不同的层次做不同的事,能让一个复杂、混乱的应用变得思路清晰,代码可以复用,并且易于测试,有利于分工合作,构建更大、更复杂的应用。
一个应用要包括哪些功能?表现(view)、处理数据的逻辑(model)以及数据映射到表现层的逻辑(presenter or controller),数据在这三层之间流通(MVVM模式通过数据双向绑定实现view和model同步)。
React 根据state来render,它只是个状态机,并没有解决管理状态的问题。我们在单纯的使用React来写组件的时候,经常会遇到组件间通信和管理组件state的问题,前者常用的解决办法就是把数据提到父组件共享;后者管理state简单的情况还行,一复杂就很麻烦且容易出错,再遇到一些需要异步处理的操作,想想就头皮发麻。
当你开发中遇到一些反人类的操作时,试着去想如何改变一下思路让它变得更简单,别耐着性子安慰自己开发就是这样 :)
解决方案
Redux 正是用来解决大型React应用所面临的状态管理、数据流通、异步处理、测试、团队合作等问题:
Redux 用单一的object tree来表示整个应用的state,这个表示state的对象树被放在唯一的store 中,state相当于store的快照;所有组件都会通过API拿到这个state,各取所需;
Redux 把页面上用户的操作或者浏览器的行为(如路由的变化)定义为一个要更新state的action
,这个action
是一个普通对象,它包含了要执行动作的类别和传递到state的数据(如果有的话),它只表明要更改state的意图,相当于一个信号,并不能直接修改state,Redux会集中处理这些信号,这个action
由你来决定何时发起;
定义好信号,你还需要根据不同的信号定义不同的逻辑函数(reducer
s)来更新state。
通过这张图来整理一下:
咳咳...比如用户点击的一个按钮,你在按钮上绑定的回调函数调用了一个(多个)action
creator,action
creator就返回了一个更新state局部数据的action
,store会根据这个(多个)action
找到对应的reducers(reducer需要做拆分),按照action
发起的顺序依次执行来更新state,每个reducer只负责更新自己关心那部分,根 reducer 把多个子 reducer 输出合并成一个单一的 state 树,生成一个新的state保存在store中,store中的state可以通过相应API传递到子组件。
这就是整个数据流。
那Redux如何处理异步操作?
Redux借鉴了中间件思想,利用可扩展的中间件来改造dispatch函数。比如redux-thunk让dispatch不仅仅可以接收action
,还可以接受函数作为参数,你可以在这个函数里完成异步操作。再如redux-saga更强大、也更复杂,在后面会讲到。
Redux 架构方法
对于React技术栈,Redux实现了react-redux库来让Redux管理React应用(其他框架也有相应的库),里面集成了一些有用的函数来把一些明确的流程自动化,如createStore
用于创建唯一store,可以把根reducer传进createStore
使store自动调用对应reducer,可以扩展中间件;提供<Provider store>
组件和connect高阶组件用来包裹render component并传递state,connect还能自动dispatch,让你只要调用action
creator就能dispatch;提供combineReducers来组合分割的reducers等。
知道这些特性,就可以配合react-router构建大型应用了:
总的思路就是:利用react-router 把应用分割为各个页面,reducer、action
creator也跟随页面分割而分割。每个路由对应的页面下都有components和containers,分别存放functional components 和class components,前者用来渲染,后者当做containers被connect包裹,containers包裹components;containers从connect得到state并映射需要的数据到子组件的props,子组件再向下传递。
具体如何构建React + Redux + react-router,我在另一篇博客里讲了。
使用这种架构,开发大型应用变得得心应手。
Redux 存在的问题
但是当我深入项目开发的时候,也逐渐发现了一些问题:
- 这种架构项目结构不够扁平化,文件嵌套比较深,思路比较复杂,搭建、写起来比较麻烦,上手有难度;
- 由于所有
action
creator都定义在页面层次上,让子组件调用必须一层一层的传递,很麻烦且非常容易出错,也很难调试; - state难以做到局部更新(这个可以用
reselect
) - Redux只是传递了一种思路,定义了几个简单的API,很灵活,架构方式不固定,设计方式不固定(如:如何设计state树)但这也是它的缺点,新人往往看完一遍还是不知道怎么做,对新人不友好
总之,redux可以胜任复杂数据流的应用,但是也比较难,前期架构比较麻烦,适合有经验的人。
使用redux-saga处理异步操作
Redux 倡导action
和reducer尽可能"纯净",没有什么“副作用”。可是像一些异步操作比如获取数据是必须的,在哪处理这些副作用呢?redux 把这些"不纯净的"任务交给了中间件,通过 向createStore
里应用中间件,在交由store处理action
之前就可以对其完成一些其他的操作:
而redux-saga 是Redux一个强大但并不复杂的用于异步处理的中间件。
它的思路是什么?相比其他redux异步中间件如redux-thunk、redux-promise有什么不同?
先看名字来理解:saga,这个术语常用于CQRS架构,代表查询与责任分离。
没错,就是查询(dispatch)与责任(sagas)分离。saga提供了action
监听函数,只需在组件里dispatch 相应type的action
,就可以自动调用你定义好的对应这个action
的异步处理函数(sagas)来完成任务,保证了只在组件里dispatch action
来发起异步操作而不是redux-thunk、redux-promise的调用action
creators。
另外一大特色就是redux-saga做到了异步代码以同步方式写,非常直观方便,怎么做到的呢?它是利用了ES6新魔法Generator迭代器,可以完美解决异步回调地狱,让你以同步方式写异步。saga正是利用Generator特性让其处理异步变得非常方便又容易理解。这是一个常见的请求后台数据的异步操作,感受一下:
function *fetchNodeDetailByNodeId({ payload: { nodeId } }, { call, put }) { try { const { data, status }= yield call(fetchNodeDetailByNodeId, nodeId) if (data && status.errmsg === 'success') { yield put({ type: 'setStates', payload: { nodeDetailData: data, }, }); } else { message.info('开了个小差,再试一次吧..'); } } catch (error) { console.log(error); } },
call 和 put 是saga的API,相当于dispatch,但是并不是真正执行dispatch,只是发送你指定的指令,交由saga中间件来执行这个指令。这样看来,这个saga函数就是一些指令的集合,称为effects,副作用,用来描述任务
为啥要描述指令而不直接调用呢?这样是因为易于测试,如果直接调用,你还得模拟调用的函数,详见redux-saga文档。
我觉得redux-saga相比于其他中间件的优点:
- 查询与责任分离,保证了
action
的纯洁性,符合redux设计思想 - 实现以同步方式写异步操作,容易理解,逻辑清晰
- 通过发送指令而不是直接调用让异步操作变得容易测试
- 监听、执行自动化
- 提供了丰富强大的指令来完成复杂的操作,比如无阻塞调用,同时执行多个任务等
讲道理,任何redux异步操作都可以让saga这个中间件来完成,非常复杂的同样可以胜任,并且很容易理解(异步操作以同步方式写)和测试。再配合dva,可以减轻redux的复杂度同时完成更强大的功能。
这样以来,redux配合saga,就可以让它们各司其职,整个思路也变得清晰起来:
redux 倡导action
和reducer要纯洁,那就让所有异步操作这些不纯洁的任务交给saga,reducer不用变,还是纯函数;定义好对应action
的sagas专门用来处理异步操作,我只要在组件需要的地方里dispatch 纯action
就行了,符合redux设计思想。
总结
使用redux来管理应用状态适用于复杂的应用,而复杂的应用会有复杂的异步处理,异步处理不要用redux的action
creator,它不是用来做这个的,也违背了redux设计思想,redux把这些任务交给了异步中间件,应该由它们来完成。使用redux saga是一个推荐的选择,它懂redux,也懂你需要什么。另外,既然你用到了saga,不妨试试dva架构,5分钟上手,值得一试。