Redux异步处理之Redux-thunk与Redux-saga

Redux异步处理之 Redux-thunk与Redux-saga

Redux异步

众所周知,redux中所有的dispatch都是同步的,而处理ajax或fetch之类的异步请求一般要分为三步。

Action:

需要写三个action来处理——fetching、fetched、error这三步,请看代码:

1
2
3
{ type:'FETCH_POSTS_REQUEST'}//发起请求
{ type:'FETCH_POSTS_FAILURE', payload: 'errorInfo' }//请求失败
{ type:'FETCH_POSTS_SUCCESS', payload:{data}//请求成功并且获取到数据

Reducer:

同样需要写三个对应的reduce来进行对state的处理以及合并,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let reducer = (state = initialState, action) => {
switch(action.type) {
//发起请求时reducer的处理
case 'FETCH_POSTS_REQUEST': {
return {...state, fetching: true}
break;
}
//请求失败时的处理
case 'FETCH_POSTS_FAILURE': {
return {...state, fetching: false, error: action.payload}
break;
}
//请求成功
case 'FETCH_POSTS_SUCCESS': {
return {...state, fetching: false, fetched: true, users: action.payload}
break;
}
}
return state;
}

如何进行dispatch

由于dispatch是同步操作,reducer中又是以纯函数的形式进行代码编写,不能往里面加入其它带有操作性质的代码,因此异步的操作我们需要借助中间件来完成。
redux中 applyMiddleware 用法如下:

1
2
3
4
const store = createStore(
reducer,
applyMiddleware(...middlewares)
);

为了了解中间件到底做了些什么,先来看下源码:

applyMiddleware:

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
function applyMiddleware() {
for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
middlewares[_key] = arguments[_key];
}//定义一个middlewares数组,长度和入参相同,并且将入参传入该数组中
return function (createStore) {//这里的creatStore即前面的creatStore方法
return function (reducer, preloadedState, enhancer) {//这里的enhancer可以就当成是中间件
var store = createStore(reducer, preloadedState, enhancer);
var _dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: function dispatch(action) {
return _dispatch(action);
}
};
chain = middlewares.map(function (middleware) {
return middleware(middlewareAPI);
});//我们可以看下chain里面是个什么东西
_dispatch = compose.apply(undefined, chain)(store.dispatch);//嵌套执行中间件的组合逻辑(因此才有中间件执行顺序一说)
return _extends({}, store, {
dispatch: _dispatch //说白了中间件实际上就是改造了dispatch
});
};
};
}

compose:

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
/*
* Composes single-argument functions from right to left. The rightmost
* function can take multiple arguments as it provides the signature for
* the resulting composite function.
*
* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing the argument functions
* from right to left. For example, compose(f, g, h) is identical to doing
* (...args) => f(g(h(...args))).
*/
function compose() {
for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
funcs[_key] = arguments[_key];
}
if (funcs.length === 0) {
return function (arg) {
return arg;
};
}
//如果长度为1,返回该函数
if (funcs.length === 1) {
return funcs[0];
}
//核心语句reduce嵌套执行所有的函数最后返回一个最终函数
return funcs.reduce(function (a, b) {
return function () {
return a(b.apply(undefined, arguments));
};
});
}

综上其实compose嵌套执行的函数实际上就是类似下面这个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function f1(next) {
return function() {
console.log('f1 start')
next()
console.log('f1 end')
}
}
function f2(next) {
return function() {
console.log('f2 start')
next()
console.log('f2 end')
}
}
function f() {
console.log('heart')
}
f1(f2(f))() //所以最后的综合逻辑函数就是类似这种M1(M2(dispatch))(action)

creatStore:

中间件有关的主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
......

redux的applyMiddle就分析到这边,在了解了这个得基础上,下面就来介绍两个常用的异步流处理的中间件,redux-thunkredux-saga,本文就这两个中间件进行探讨和比较其中优劣。

认识Redux-thunk

redux-thunk的官方文档是这样介绍的:

1
从异步的角度,Thunk 是指一切都就绪的会返回某些值的函数。你不用传任何参数,你只需调用它,它便会返回相应的值。

本栗子结合thunk的使用方式如下:

1
2
3
4
5
6
7
8
9
10
//此处dispatch的不是一个action对象而是一个函数
store.dispatch((url)=>(dispatch)=>{
dispatch({
type:'FETCH_POSTS_REQUEST'
});//发起请求(象征性的触发一下,表示我准备开始请求了,其实个人感觉不写都无所谓,只是改变了一下fetching的值而已#滑稽)
fetch(url).then(data => dispatch({
type:'FETCH_POSTS_SUCCESS',
payload:data
}));//请求成功后更新state
})

因此,从上述代码可以看出redux-thunk 的主要思想是扩展 action,使得 action 从一个对象变成一个函数,并且触发dispatch都是从UI进行触发,逻辑都是写在UI。

那么为什么thunk可以传递一个函数,这里我们来看下源码:

1
2
3
4
5
6
7
8
9
10
11
12
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
//这里的意思是指如果这个action是函数,那么就执行它,否则next
return next(action);//将此action传递到下一个middleware
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

但由此可以看出redux-thunk的缺点:

  • action 虽然扩展了,但因此变得复杂,后期可维护性降低;
  • thunks 内部测试逻辑比较困难,需要mock所有的触发函数( 主要因素);
  • 协调并发任务比较困难,当自己的 action 调用了别人的 action,别人的 action 发生改动,则需要自己主动修改;
  • 业务逻辑会散布在不同的地方:启动的模块,组件以及thunks内部。(主要因素);

完整代码

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
import {applyMiddleware, createStore} from 'redux';
import axios from 'axios';
import thunk from 'redux-thunk';
const initialState = { fetching: false, fetched: false, users: [], error: null }
const reducer = (state = initialState, action) => {
switch(action.type) {
case 'FETCH_POSTS_REQUEST': {
return {...state, fetching: true}
break;
}
case 'FETCH_POSTS_FAILURE': {
return {...state, fetching: false, error: action.payload}
break;
}
case 'FETCH_POSTS_SUCCESS': {
return {...state, fetching: false, fetched: true, users: action.payload}
break;
}
}
return state;
}
const middleware = applyMiddleware(thunk);
store.dispatch((dispatch) => {
dispatch({type: 'FETCH_POSTS_REQUEST'});
axios.get('./data.json').then((response) => {
dispatch({type: 'FETCH_POSTS_SUCCESS', payload: response.data})
})
.catch((err) => {
dispatch({type: 'FETCH_POSTS_FAILURE', payload: err})
})
});

认识Redux-saga

首先什么是saga?
saga,这个术语常用于CQRS架构,代表查询与责任分离。

saga的作者是个搞房地产的,业余时间写写代码,和阮一峰有点像。

官方的介绍是这样的:

1
2
redux-saga 是一个旨在使应用程序副作用(即数据获取等异步操作和获取浏览器缓存等非纯函数操作)更容易管理,执行更有效率,容易测试和解决错误的库。这个构思模型是一个saga就像一个应用程序中仅仅用于解决这些副作用的单线程。
redux-saga是一个redux中间件,具有完全访问redux应用状态,也可以迅速处理redux action,这意味着这个线程可以被主程序中的正常redux action启动、暂停和取消。它利用了es6之中的generators的特点来使这些异步数据流容易读取、写入和测试。这样做,这些异步数据流看起来就像是你的标准的同步JavaScript代码。(类似于async/await),除此之外,generators有一些我们需要的令人惊叹的特征。

redux-saga 将异步任务进行了集中处理,且方便测试。

所有的东西都必须被封装在 sagas 中。sagas 包含3个部分,用于联合执行任务:

  • worker saga :做所有的工作,如调用 API,进行异步请求,并且获得返回结果。
  • watcher saga :监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务。
  • root saga :立即启动 sagas 的唯一入口

使用方法

  • 使用createSagaMiddleware方法创建saga 的Middleware,然后在创建的redux的store时,使用applyMiddleware函数将创建的sagaMiddleware实例绑定到store上,最后可以调用saga Middleware的run函数来执行某个或者某些Middleware。
  • 在saga的Middleware中,可以使用takeEvery或者takeLatest等API来监听某个action,当某个action触发后,saga可以使用call、fetch等api发起异步操作,操作完成后使用put函数触发action,同步更新state,从而完成整个State的更新。
    首先需要启动saga,启动saga一般都写在入口文件中,下面是个栗子:
1
2
3
4
5
6
7
8
9
10
11
12
13
import { createStore, applyMiddleware} from 'redux';
import createSagaMiddleware from 'redux-saga';
import appReducer from './reducers';
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const store = createStore(appReducer,applyMiddleware(...middlewares));
sagaMiddleware.run(rootSaga);//saga一旦执行就会永远执行下去
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
);

然后,就可以在 sagas 文件夹中集中写 saga 文件了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { take, fork, call, put } from 'redux-saga/effects';
//执行函数即work saga
function* fetchUrl(url) {
try{//利用try-catch来捕获异常
const data = yield call(fetch, url); // 指示中间件调用 fetch 异步任务
yield put({ type: 'FETCH_POSTS_SUCCESS', payload:data }); // 指示中间件发起一个 action 到 Store
}catch(e){
yield put({ type: 'FETCH_POSTS_FAILURE', payload:error })
}
}
// 监听函数即watch saga
function* watchFetchRequests() {
while(true) {
const action = yield take('FETCH_POSTS_REQUEST'); // 指示中间件等待 Store 上指定的 action,即监听 action
yield fork(fetchUrl, action.url); // 指示中间件以无阻塞调用方式执行 fetchUrl
}
}

在 redux-saga 中的基本概念就是:sagas 自身不真正执行副作用(如函数 call),但是会构造一个需要执行副作用的描述。中间件会执行该副作用并把结果返回给 generator 函数。
对于sages ,采用 Generator 函数来 yield Effects(包含指令的文本对象)。Generator 函数的作用是可以暂停执行,再次执行的时候从上次暂停的地方继续执行。Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。你可以通过使用 effects API 如 fork,call,take,put,cancel 等来创建 Effect。

关于Effect官方是这样解释的:

在 redux-saga 的世界里,Sagas 都用 Generator 函数实现。我们从 Generator 里 yield 纯 JavaScript 对象以表达 Saga 逻辑。 我们称呼那些对象为 Effect。Effect 是一个简单的对象,这个对象包含了一些给 middleware 解释执行的信息。 你可以把 Effect 看作是发送给middleware 的指令以执行某些操作(调用某些异步函数,发起一个 action 到 store)。

对上述例子的说明:

  • 引入的 redux-saga/effects 都是纯函数,每个函数构造一个特殊的对象,其中包含着中间件需要执行的指令,如:call(fetchUrl, url) 返回一个类似于 {type: CALL, function: fetchUrl, args: [url]} 的对象。
  • 在 watcher saga watchFetchRequests中:
    首先 yield take(‘FETCH_REQUEST’) 来告诉中间件我们正在等待一个类型为 FETCH_REQUEST 的 action,然后中间件会暂停执行
    wacthFetchRequests generator 函数,直到 FETCH_REQUEST action 被 dispatch。一旦我们获得了匹配的 action,中间件就会恢复执行 generator 函数。下一条指令 fork(fetchUrl, action.url) 告诉中间件去无阻塞调用一个新的 fetchUrl 任务,action.url 作为 fetchUrl 函数的参数传递。中间件会触发 fetchUrl generator 并且不会阻塞 watchFetchRequests。当fetchUrl 开始执行的时候,watchFetchRequests会继续监听其它的 watchFetchRequests actions。当然,JavaScript 是单线程的,redux-saga 让事情看起来是同时进行的。
  • 在 worker saga fetchUrl 中,call(fetch,url) 指示中间件去调用 fetch 函数,同时,会阻塞fetchUrl 的执行,中间件会停止 generator 函数,直到 fetch 返回的 Promise 被 resolved(或 rejected),然后才恢复执行 generator 函数。

这里我们可以来看一波源码,搞清楚这个Effect到底是什么。
对于Effect对象的定义,写在了 redux-saga/src/internal/io.js 文件中,下面是Effect的定义。

1
const effect = (type, payload) => ({ [IO]: true, [type]: payload });

很简单就一句话,表明这个effect其实就是返回了一个对象。

接下来看看所谓的put和call到底是个什么东西

put
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function put(channel, action) {
if (process.env.NODE_ENV === 'development') {
if (arguments.length > 1) {
check(channel, is.notUndef, 'put(channel, action): argument channel is undefined')
check(channel, is.channel, `put(channel, action): argument ${channel} is not a valid channel`)
check(action, is.notUndef, 'put(channel, action): argument action is undefined')
} else {
check(channel, is.notUndef, 'put(action): argument action is undefined')
}
}
if (is.undef(action)) {
action = channel
channel = null
}
return effect(PUT, { channel, action })
}
call
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function getFnCallDesc(meth, fn, args) {
if (process.env.NODE_ENV === 'development') {
check(fn, is.notUndef, `${meth}: argument fn is undefined`)
}
let context = null
if (is.array(fn)) {
[context, fn] = fn
} else if (fn.fn) {
({ context, fn } = fn)
}
if (context && is.string(fn) && is.func(context[fn])) {
fn = context[fn]
}
if (process.env.NODE_ENV === 'development') {
check(fn, is.func, `${meth}: argument ${fn} is not a function`)
}
return { context, fn, args }
}
export function call(fn, ...args) {
return effect(CALL, getFnCallDesc('call', fn, args))
}

出乎意料都是只返回了一个纯对象( 先不管细节 )。
effect返回的纯对象由于generate函数的机制会将yield的控制权交给外部,用来给generator外层的执行容器task( 这东西我讲不清楚所以就不讲了 )发送一个信号,告诉task该做什么。task在接收到effect发出的指令后将会执行下面这段函数。

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
40
41
42
43
44
45
46
47
48
49
50
function next(arg, isErr) {
// Preventive measure. If we end up here, then there is really something wrong
if (!mainTask.isRunning) {
throw new Error('Trying to resume an already finished generator')
}
try {
let result
if (isErr) {
result = iterator.throw(arg)
} else if (arg === TASK_CANCEL) {
/**
getting TASK_CANCEL automatically cancels the main task
We can get this value here
- By cancelling the parent task manually
- By joining a Cancelled task
**/
mainTask.isCancelled = true
/**
Cancels the current effect; this will propagate the cancellation down to any called tasks
**/
next.cancel()
/**
If this Generator has a `return` method then invokes it
This will jump to the finally block
**/
result = is.func(iterator.return) ? iterator.return(TASK_CANCEL) : { done: true, value: TASK_CANCEL }
} else if (arg === CHANNEL_END) {
// We get CHANNEL_END by taking from a channel that ended using `take` (and not `takem` used to trap
End of channels)
result = is.func(iterator.return) ? iterator.return() : { done: true }
} else {
result = iterator.next(arg)//这里将会执行generator并将结果赋值给result
}
if (!result.done) {//这里会判断这个generator是否执行完毕
runEffect(result.value, parentEffectId, '', next) //这里的runEffect就是各种执行结果的返回(全部流程到此结束)
} else {
/**
This Generator has ended, terminate the main task and notify the fork queue
**/
mainTask.isMainRunning = false
mainTask.cont && mainTask.cont(result.value)
}
} catch (error) {
if (mainTask.isCancelled) {
log('error', `uncaught at ${name}`, error.message)
}
mainTask.isMainRunning = false
mainTask.cont(error, true)
}
}

Redux-saga优点

  • 声明式 Effects:所有的操作以JavaScript对象的方式被 yield,并被 middleware 执行。使得在 saga 内部测试变得更加容易,可以通过简单地遍历 Generator 并在 yield 后的成功值上面做一个 deepEqual 测试。
  • 高级的异步控制流以及并发管理:可以使用简单的同步方式描述异步流,并通过 fork(无阻塞) 实现并发任务。
  • 架构上的优势:将所有的异步流程控制都移入到了 sagas,UI 组件不用执行业务逻辑,只需 dispatch action 就行,增强组件复用性。

Edit by AmamiRyoin

觉得不错的话可以打赏哦