盒子
盒子
文章目录󰁋
  1. 一、redux-thunk
    1. 1.1 redux的副作用处理
    2. 1.2 redux-thunk源码
    3. 1.3 redux-thunk的缺点
  2. 二、redux-saga 简介
  3. 三、redux-saga使用案例
  4. 四、redux-saga使用细节
    1. 4.1 声明式的Effect
    2. 4.2 Effect提供的具体方法
      1. 4.2.1 take
      2. 4.2.2 call(apply)
      3. 4.2.3 put
      4. 4.2.4 select
      5. 4.2.5 fork
      6. 4.2.6 takeEvery和takeLatest
  5. 五、案例分析一
    1. 5.1 LoginPanel(登陆页)
    2. 5.2 LoginSuccess
  6. 六、案例分析二
    1. 6.1 配置saga信息
    2. 6.2 配置reduce
    3. 6.3 处理action
    4. 6.4 处理sagas
  7. 七、总结

浅析redux-saga中间件及用法

一、redux-thunk

1.1 redux的副作用处理

redux中的数据流大致是

1
UI—————>action(plain)—————>reducer——————>state——————>UI

image.png

  • redux是遵循函数式编程的规则,上述的数据流中,action是一个原始js对象(plain object)且reducer是一个纯函数,对于同步且没有副作用的操作,上述的数据流起到可以管理数据,从而控制视图层更新的目的
  • 如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的js对象。如何处理副作用操作,在redux中选择在发出action,到reducer处理函数之间使用中间件处理副作用

redux增加中间件处理副作用后的数据流大致如下:

1
UI——>action(side function)—>middleware—>action(plain)—>reducer—>state—>UI

image.png

在有副作用的action和原始的action之间增加中间件处理,从图中我们也可以看出,中间件的作用就是:

  • 转换异步操作,生成原始的action,这样,reducer函数就能处理相应的action,从而改变state,更新UI

1.2 redux-thunk源码

在redux中,thunk是redux作者给出的中间件,实现极为简单,10多行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}

return next(action);
};
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

这几行代码做的事情也很简单,判别action的类型,如果action是函数,就调用这个函数,调用的步骤为

1
action(dispatch, getState, extraArgument);

发现实参为dispatchgetState,因此我们在定义actionthunk函数是,一般形参为dispatchgetState

1.3 redux-thunk的缺点

thunk的缺点也是很明显的,thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使得redux可以接受函数作为action,但是函数的内部可以多种多样。比如下面是一个获取商品列表的异步操作所对应的action

1
2
3
4
5
6
7
8
9
10
11
12
13
export default ()=>(dispatch)=>{
fetch('/api/goodList',{ //fecth返回的是一个promise
method: 'get',
dataType: 'json',
}).then(function(json){
var json=JSON.parse(json);
if(json.msg==200){
dispatch({type:'init',data:json.data});
}
},function(error){
console.log(error);
});
};

从这个具有副作用的action中,我们可以看出,函数内部极为复杂。如果需要为每一个异步操作都如此定义一个action,显然action不易维护

action不易维护的原因

  • action的形式不统一
  • 就是异步操作太为分散,分散在了各个action

二、redux-saga 简介

redux-saga是一个 redux中间件,它具有如下特性

  • 集中处理 redux 副作用问题。
  • 被实现为 generator
  • redux-thunk 中间件。
  • watch/worker(监听->执行) 的工作形式

redux-saga的优点

  • 集中处理了所有的异步操作,异步接口部分一目了然
  • action是普通对象,这跟redux同步的action一模一样
  • 通过Effect,方便异步接口的测试
  • 通过workerwatcher可以实现非阻塞异步调用,并且同时可以实现非阻塞调用下的事件监听
  • 异步操作的流程是可以控制的,可以随时取消相应的异步操作

基本用法

  • 使用createSagaMiddleware方法创建sagaMiddleware,然后在创建的reduxstore时,使用applyMiddleware函数将创建的saga Middleware实例绑定到store上,最后可以调用saga Middlewarerun函数来执行某个或者某些Middleware
  • sagaMiddleware中,可以使用takeEvery或者takeLatestAPI来监听某个action,当某个action触发后,saga可以使用call发起异步操作,操作完成后使用put函数触发action,同步更新state,从而完成整个State的更新。

三、redux-saga使用案例

  • redux-saga是控制执行的generator,在redux-sagaaction是原始的js对象,把所有的异步副作用操作放在了saga函数里面。这样既统一了action的形式,又使得异步操作集中可以被集中处理
  • redux-saga是通过genetator实现的,如果不支持generator需要通过插件babel-polyfill转义。我们接着来实现一个输出hellosaga的例子

创建一个helloSaga.js文件

1
2
3
export function * helloSaga() {
console.log('Hello Sagas!');
}

在redux中使用redux-saga中间件

main.js

1
2
3
4
5
6
7
8
9
10
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { helloSaga } from './sagas'
const sagaMiddleware=createSagaMiddleware();
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(helloSaga);
//会输出Hello, Sagas!

和调用redux的其他中间件一样,如果想使用redux-saga中间件,那么只要在applyMiddleware中调用一个createSagaMiddleware的实例。唯一不同的是需要调用run方法使得generator可以开始执行

四、redux-saga使用细节

4.1 声明式的Effect

redux-saga中提供了一系列的api,比如takeputallselectAPI,在redux-saga中将这一系列的api都定义为Effect。这些Effect执行后,当函数resolve时返回一个描述对象,然后redux-saga中间件根据这个描述对象恢复执行generator中的函数

redux-thunk的大体过程

action1(side function)—>redux-thunk监听—>执行相应的有副作用的方法—>action2(plain object)

image.png

转化到action2是一个原始js对象形式的action,然后执行reducer函数就会更新store中的state

redux-saga的大体过程

action1(plain object)——>redux-saga监听—>执行相应的Effect方法——>返回描述对象—>恢复执行异步和副作用函数—>action2(plain object)

image.png

对比redux-thunk我们发现,redux-saga中监听到了原始js对象action,并不会马上执行副作用操作,会先通过Effect方法将其转化成一个描述对象,然后再将描述对象,作为标识,再恢复执行副作用函数

4.2 Effect提供的具体方法

下面来介绍几个Effect中常用的几个方法,从低阶的API,比如takecall(apply)forkputselect等,以及高阶API,比如takeEverytakeLatest

1
import {take,call,put,select,fork,takeEvery,takeLatest} from 'redux-saga/effects'

4.2.1 take

take这个方法,是用来监听action,返回的是监听到的action对象。比如

1
2
3
const loginAction = {
type:'login'
}

UI Componentdispatch一个action

1
dispatch(loginAction)

在saga中使用:

1
const action = yield take('login');

可以监听到UI传递到中间件的Action,上述take方法的返回,就是dipath的原始对象。一旦监听到login动作,返回的action为:

1
2
3
{
type:'login'
}

4.2.2 call(apply)

callapply方法与js中的callapply相似,我们以call方法为例

1
call(fn, ...args)

call方法调用fn,参数为args,返回一个描述对象。不过这里call方法传入的函数fn可以是普通函数,也可以是generatorcall方法应用很广泛,在redux-saga中使用异步请求等常用call方法来实现

1
yield call(fetch,'/userInfo',username)

4.2.3 put

redux-saga做为中间件,工作流是这样的

1
UI——>action1————>redux-saga中间件————>action2————>reducer..

从工作流中,我们发现redux-saga执行完副作用函数后,必须发出action,然后这个actionreducer监听,从而达到更新state的目的。相应的这里的put对应与redux中的dispatch,工作流程图如下

image.png

可以看出redux-saga执行副作用方法转化action时,put这个Effect方法跟redux原始的dispatch相似,都是可以发出action,且发出的action都会被reducer监听到。put的使用方法

1
yield put({type:'login'})

4.2.4 select

put方法与redux中的dispatch相对应,同样的如果我们想在中间件中获取state,那么需要使用selectselect方法对应的是redux中的getState,用户获取store中的state,使用方法:

1
const id = yield select(state => state.id);

4.2.5 fork

fork方法相当于web workfork方法不会阻塞主线程,在非阻塞调用中十分有用

4.2.6 takeEvery和takeLatest

takeEverytakeLatest用于监听相应的动作并执行相应的方法,是构建在takefork上面的高阶api,比如要监听login动作,好用takeEvery方法可以

1
takeEvery('login',loginFunc)
  • takeEvery监听到login的动作,就会执行loginFunc方法,除此之外,takeEvery可以同时监听到多个相同的action
  • takeLatest方法跟takeEvery是相同方式调用
1
takeLatest('login',loginFunc)

takeLatest不同的是,takeLatest是会监听执行最近的那个被触发的action

五、案例分析一

接着我们来实现一个redux-saga样例,存在一个登陆页,登陆成功后,显示列表页,并且,在列表页,可以点击登出,返回到登陆页。例子的最终展示效果如下

image.png

样例的功能流程图为

image.png

5.1 LoginPanel(登陆页)

输入时时保存用户名和密码

  • 用户名输入框和密码框onchange时触发的函数为
1
2
3
4
5
6
changeUsername:(e)=>{
dispatch({type:'CHANGE_USERNAME',value:e.target.value});
},
changePassword:(e)=>{
dispatch({type:'CHANGE_PASSWORD',value:e.target.value});
}

在函数中最后会dispatch两个action:CHANGE_USERNAME和CHANGE_PASSWORD

  • saga.js文件中监听这两个方法并执行副作用函数,最后put发出转化后的action,给reducer函数调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function * watchUsername(){
while(true){
const action= yield take('CHANGE_USERNAME');
yield put({type:'change_username',
value:action.value});
}
}
function * watchPassword(){
while(true){
const action=yield take('CHANGE_PASSWORD');
yield put({type:'change_password',
value:action.value});
}
}

最后在reducer中接收到redux-sagaput方法传递过来的action:change_usernamechange_password,然后更新state

监听登陆事件判断登陆是否成功

在UI中发出的登陆事件为

1
2
3
toLoginIn:(username,password)=>{
dispatch({type:'TO_LOGIN_IN',username,password});
}

登陆事件的action为:TO_LOGIN_IN.对于登入事件的处理函数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
while(true){
//监听登入事件
const action1=yield take('TO_LOGIN_IN');
const res=yield call(fetchSmart,'/login',{
method:'POST',
body:JSON.stringify({
username:action1.username,
password:action1.password
})
if(res){
put({type:'to_login_in'});
}
});

在上述的处理函数中,首先监听原始动作提取出传递来的用户名和密码,然后请求是否登陆成功,如果登陆成功有返回值,则执行putaction:to_login_in

5.2 LoginSuccess

(登陆成功列表展示页)

  • 登陆成功后的页面功能包括:
    • 获取列表信息,展示列表信息
    • 登出功能,点击可以返回登陆页面

获取列表信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {delay} from 'redux-saga';

function * getList(){
try {
yield delay(3000);
const res = yield call(fetchSmart,'/list',{
method:'POST',
body:JSON.stringify({})
});
yield put({type:'update_list',list:res.data.activityList});
} catch(error) {
yield put({type:'update_list_error', error});
}
}

为了演示请求过程,我们在本地mock,通过redux-saga的工具函数delaydelay的功能相当于延迟xx秒,因为真实的请求存在延迟,因此可以用delay在本地模拟真实场景下的请求延迟

登出功能

1
2
const action2=yield take('TO_LOGIN_OUT');
yield put({type:'to_login_out'});

与登入相似,登出的功能从UI处接受action:TO_LOGIN_OUT,然后转发action:to_login_out

完整的实现登入登出和列表展示的代码

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
function * getList(){
try {
yield delay(3000);
const res = yield call(fetchSmart,'/list',{
method:'POST',
body:JSON.stringify({})
});
yield put({type:'update_list',list:res.data.activityList});
} catch(error) {
yield put({type:'update_list_error', error});
}
}

function * watchIsLogin(){
while(true){
//监听登入事件
const action1=yield take('TO_LOGIN_IN');

const res=yield call(fetchSmart,'/login',{
method:'POST',
body:JSON.stringify({
username:action1.username,
password:action1.password
})
});

//根据返回的状态码判断登陆是否成功
if(res.status===10000){
yield put({type:'to_login_in'});
//登陆成功后获取首页的活动列表
yield call(getList);
}

//监听登出事件
const action2=yield take('TO_LOGIN_OUT');
yield put({type:'to_login_out'});
}
}

通过请求状态码判断登入是否成功,在登陆成功后,可以通过

1
yield call(getList)

注意call方法调用是会阻塞主线程的,具体来说

  • 在call方法调用结束之前,call方法之后的语句是无法执行的
  • 如果call(getList)存在延迟,call(getList)之后的语句 const action2=yieldtake('TO_LOGIN_OUT')call方法返回结果之前无法执行
  • 在延迟期间的登出操作会被忽略

image.png

无阻塞调用

1
yield call(getList)

修改为

1
yield fork(getList)

通过fork方法不会阻塞主线程,在白屏时点击登出,可以立刻响应登出功能,从而返回登陆页面

六、案例分析二

6.1 配置saga信息

src/store/configureStore.js

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
import { createStore, applyMiddleware, compose } from 'redux'
// import {createLogger } from 'redux-logger'
import createHistory from 'history/createBrowserHistory'
import createSagaMiddleware from 'redux-saga';
import { routerMiddleware } from 'react-router-redux'
import rootSaga from '../sagas'
import rootReducer from '../reducers/'

export const history = createHistory()

const middleware = routerMiddleware(history)

//创建saga middleware
const sagaMiddleware = createSagaMiddleware();


const configureStore = preloadedState => {
// 安装 Redux-DevTools Chrome 插件后可用 composeEnhancers()
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

const store = createStore(
rootReducer,
preloadedState,
composeEnhancers(
applyMiddleware(sagaMiddleware,middleware)
)
)
sagaMiddleware.run(rootSaga);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextRootReducer = require('../reducers').default
store.replaceReducer(nextRootReducer)
})
}

return store
}


export default configureStore

6.2 配置reduce

1
2
3
4
5
6
7
8
9
10
// src/reducers/index.js
import {combineReducers} from 'redux'
import {routerReducer as routing} from 'react-router-redux'

const rootReducer = combineReducers({
routing,
poetry : require('./poetry').default
})

export default rootReducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/reducers/poetry.js

import * as ActionTypes from '../actions'

export default (state = {
fetching:false,
error:false,
errMsg:'',
data:[]
},action) => {
if(action.type === ActionTypes.FETCH_POETRY_REQUEST){
return Object.assign({...state,fetching:true,errMsg:''})
}else if(action.type === ActionTypes.FETCH_POETRY_SUCCESS){
const data = action.payload.data
return Object.assign({...state,fetching:false,data,errMsg:''})
}else if(action.type === ActionTypes.FETCH_POETRY_FAILURE){
return Object.assign({...state,fetching:false,error:true,errMsg:action.payload.errMsg})
}
return state
}

6.3 处理action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/action/index.js

import { createAction } from 'redux-actions';

export const COMMON_FETCHING = 'COMMON_FETCHING'
export const COMMON_OVER = 'COMMON_OVER'
export const MSG_SHOW = 'MSG_SHOW'
export const MSG_INIT = 'MSG_INIT'
export const POP_LOGIN = 'POP_LOGIN'
export const initMsg = () => ({type : MSG_INIT})


// 相比用thunk多了一步 多了个action 来触发saga woker
export const FETCH_POETRY_REQUEST = 'FETCH_POETRY_REQUEST'
export const FETCH_POETRY_SUCCESS = 'FETCH_POETRY_SUCCESS'
export const FETCH_POETRY_FAILURE = 'FETCH_POETRY_FAILURE'
export const fetchPoetryRequest = createAction(FETCH_POETRY_REQUEST)
export const fetchPoetrySuccess = createAction(FETCH_POETRY_SUCCESS)
export const fetchPoetryFauilure= createAction(FETCH_POETRY_FAILURE)

6.4 处理sagas

1
2
3
4
5
6
7
8
9
10

// src/sagas/index.js

import { all } from 'redux-saga/effects'

export default function* rootSaga() {
yield all([
...require('./fetchPoetry').default
])
}
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

// src/fetchPoetry.js

import {put,take,call,fork,takeEvery,select} from 'redux-saga/effects'
import {delay} from 'redux-saga'
import * as api from '../api'
import * as actionTypes from '../actions/'

// saga worker 监听FETCH_POETRY_REQUEST动作触发执行相应操作
function* fetchPoetrySaga(){
// yield delay(100)
// ======== 写法一 =========
// yield takeEvery(actionTypes.FETCH_POETRY_REQUEST,function*(action){
// // 调用this.props.fetchPoetryRequest({user:'poetries',age:23}) 传参进来这里
// // 也可以通过这样获取state中的参数 const state = yield select()
// const {user,age} = action
// try{
// const data = yield call(api.get({
// url:'/mock/5b7fd63f719c7b7241f4e2fa/tangshi/tang-shi'
// }))
// yield put(actionTypes.fetchPoetrySuccess({data:data.data.data}))
// }catch(error){
// yield put(actionTypes.fetchPoetryFauilure({errMsg:error.message}))
// }

// })
// === 写法二====
while(true){
// 当dispatch({type:FETCH_POETRY_REQUEST})的时候被这里监听 执行对应的请求
const {user,age} = yield take(actionTypes.FETCH_POETRY_REQUEST)
try{
const data = yield call(api.get({
url:'/mock/5b7fd63f719c7b7241f4e2fa/tangshi/tang-shi'
}))
yield put(actionTypes.fetchPoetrySuccess({data:data.data.data}))
}catch(error){
yield put(actionTypes.fetchPoetryFauilure({errMsg:error.message}))
}
}

}


// 导出所有的saga
export default [
fork(fetchPoetrySaga)
]

完整代码例子 https://github.com/poetries/redux-saga-template

七、总结

redux-saga做为redux中间件的全部优点

  • 统一action的形式,在redux-saga中,从UIdispatchaction为原始对象
  • 集中处理异步等存在副作用的逻辑
  • 通过转化effects函数,可以方便进行单元测试
  • 完善和严谨的流程控制,可以较为清晰的控制复杂的逻辑
支持一下
扫一扫,支持poetries