概述

本文介绍了对 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由你来决定何时发起;

定义好信号,你还需要根据不同的信号定义不同的逻辑函数(reducers)来更新state。

通过这张图来整理一下:

“redux 原理图”

咳咳…比如用户点击的一个按钮,你在按钮上绑定的回调函数调用了一个(多个)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 中间件”

而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特性让其处理异步变得非常方便又容易理解。这是一个常见的请求后台数据的异步操作,感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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分钟上手,值得一试。