Fork me on GitHub

redux异步历程

 理解为什么而用?

  前阵子手撕了部分redux源码,对其中一些设计思想的理解提升了不少。但是,在我自身开发的历程中还是一直有一个问题困扰着我:这些不断进化的redux异步中间件库到底是为什么出现的,它们的诉求究竟是解决一个什么样的问题?

redux-thunk

  redux-thunk是我最早接触的一个异步中间件库。它解决的核心诉求是:如何统一组织异步场景的action派发,用白话文来说就是统一在一个地方(文件目录下)进行异步action逻辑的封装。在之前<<论Redux异步流>>一文中,我们讨论过本身action进行dispatch时,是一个同步的动作。而redux-thunk中间件支持了action为函数类型的传值,这也使我们能够在这个action函数内部进行异步逻辑控制,最后在回调中同步dispatch数据。

  我们知道在redux-thunk中,对action函数的处理如下:

1
2
3
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}

  于是我们会在如action这个目录下进行对应业务逻辑的异步封装。下面以请求一个列表页数据为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const FETCH_TABLE_LIST = 'FETCH_TABLE_LIST';

// Action Creator With Side Effects
const fetchTableList = params => {
return async dispatch => {
let res = await axios.post('xx', params);
if (res && res.retCode === 'success_code') {
let { tableList = [] } = res;
dispatch({
type: FETCH_TABLE_LIST,
payload: tableList
})
}
}
}

// Reducers
const initState = {
tableList: [],
}

const tableList = (state = initState, action) => {
switch (action.type) {
case FETCH_TABLE_LIST:
return {
...state,
tableList: action.payload,
}
default:
return state;
}
}

  假如我们没有使用redux-thunk之类的中间件进行逻辑的集中管理(使dispatch接受functionpromise等类型的action),那我们上述的一些副作用就会散步在各个业务组件中,就以我们前文中的请求列表数据来说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 业务组件 xx.js
const FETCH_TABLE_LIST = 'FETCH_TABLE_LIST';

const fetchTableList = async params => {
const { dispatch } = this.props;
let res = await axios.post('xx', params);
if (res && res.retCode === 'success_code') {
let { tableList = [] } = res;
dispatch({
type: FETCH_TABLE_LIST,
payload: tableList
})
}
}

  看上去其实好像也没什么不同,这实际上就是一个将代码放在哪里管理进行复用的问题。对比redux-thunk封装后调用的写法是:this.props.dispatch(fetchTableList(params)),我们通过接受函数类型的action,使得在业务组件中的dispatch呈现显然更加清晰,副作用也不会被暴露在业务组件中。

redux-promise

  与thunk差不多,只不过接收的是Promise,它的诉求估计在于不用在业务组件层写那么多this.props.dispatch(fetchTableList(params)).then().catch()之类的代码。

  瞅瞅源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import {
isPlainObject,
isString,
} from 'lodash';

function isValidKey(key) {
return [
'type',
'payload',
'error',
'meta',
].indexOf(key) > -1;
}

// flux-standard-action库中的判断是否是一个标准action方法
export function isFSA(action) {
return (
isPlainObject(action) &&
isString(action.type) &&
Object.keys(action).every(isValidKey)
);
}

export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action) ? action.then(dispatch) : next(action);
}

return isPromise(action.payload)
? action.payload
.then(result => dispatch({ ...action, payload: result }))
.catch(error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
})
: next(action);
};
}

  大意就是,先判断是否是一个标准的action,如果不是,则判断是不是Promise类型的,是则在resolveddispatch,不是则正常走中间件的next步骤。如果是一个标准action,则对其payload内容进行判断(是否是Promise),其中进行成功和失败情况下的dispatch

  还是拿我们之前获取列表的场景,结合官方提供的用例有:

1
2
3
4
import { createAction } from 'redux-actions';
import { WebAPI } from '../utils/WebAPI';

export const fetchTableList = createAction('FETCH_TABLE_LIST', WebAPI.fetchTableList);

  注,一个标准的action具有typepayloaderrormeta四个keycreateAction能帮我们生成一个标准的FSA对象。像上面的请求API会被赋值到action.payload下。自然也会走中间件内action.payloadPromise.then.catch流程。

redux-saga

  saga,我认为它带来的核心价值有二:其一是使用generator语法处理了异步回调地狱问题;其二是使用混合式(命令+声明)编程的思想组织异步数据流,在具体异步逻辑中采用命令式编程,然后声明式调用派发action,触发sagaeffects

  通过阅读saga库的README.md,我们先理清其提供的几个API的基本功能:

  • put: 相当于数据派发时的dispatch
  • call: 配合yield,调用API,传入参数;
  • takeEvery: 类似观察者模式中观察者的角色,当我们触发dispatch时,对应typeaction会调用对应的generator函数;
  • takeLatest: 是takeEvery的一种可替代方案,调用方式相同,不过如果同一时间还有别的请求存在(比如网络问题导致的pending),旧的将会被取消,即只有最新的调用请求会保留;

话外

  分享一篇stackoverflow上关于redux middleware的讨论文章 Why do we need middleware for async flow in Redux?