Redux-saga笔记
参考资料:
- redux-saga文档
- Redux-saga
- 彻彻底底教会你使用Redux-saga(包含样例代码)
- 一篇文章介绍redux、react-redux、redux-saga总结
- 前端技术栈(三):redux-saga,化异步为同步
1. redux-thunk处理副作用的缺点
(1)redux的副作用处理
redux中的数据流大致是:
UI—————>action(plain)—————>reducer——————>state——————>UI
redux是遵循函数式编程的规则,上述的数据流中,action是一个原始js对象(plain object)且reducer是一个纯函数,对于同步且没有副作用的操作,上述的数据流起到可以管理数据,从而控制视图层更新的目的。
但是如果存在副作用,比如ajax异步请求等等,那么应该怎么做?
如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的js对象。如何处理副作用操作,在redux中选择在发出action,到reducer处理函数之间使用中间件处理副作用。
redux增加中间件处理副作用后的数据流大致如下:
UI——>action(side function)—>middleware—>action(plain)—>reducer—>state—>UI
在有副作用的action和原始的action之间增加中间件处理,从图中我们也可以看出,中间件的作用就是:
转换异步操作,生成原始的action,这样,reducer函数就能处理相应的action,从而改变state,更新UI。
(2)redux-thunk
在redux中,thunk是redux作者给出的中间件,实现极为简单,10多行代码:
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是函数,就调用这个函数,调用的步骤为:
action(dispatch, getState, extraArgument);
发现实参为dispatch和getState,因此我们在定义action为thunk函数时,一般形参为dispatch和getState。
(3)redux-thunk的缺点
thunk的缺点也是很明显的,thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使得redux可以接受函数作为action,但是函数的内部可以多种多样。比如下面是一个获取商品列表的异步操作所对应的action:
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中
2. redux-saga概述
redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,简单理解就是一个用于管理redux应用异步操作的中间件,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。
跟redux-thunk不同的是,redux-saga是控制执行的generator,在redux-saga中action是原始的js对象,把所有的异步副作用操作放在了saga函数里面。这样既统一了action的形式,又使得异步操作集中可以被集中处理。而且,redux-saga中使用声明式的Effect以及提供了更加细腻的控制流。
这意味着应用的逻辑会存在两个地方:
- reducer负责处理action的stage更新
- sagas负责协调那些复杂或者异步的操作
redux-saga特点:
- sagas是通过generator函数来创建的。因为使用了generator函数,redux-saga让你可以用同步的方式来写异步代码。
- sagas可以被看作是在后台运行的进程。sagas监听发起的action,然后决定基于这个action来做什么 (比如:是发起一个异步请求,还是发起其他的action到store,还是调用其他的sagas等 )
- 在redux-saga的世界里,所有的任务都通过用 yield Effects 来完成 ( effect可以看作是redux-saga的任务单元 )。Effects 都是简单的 javascript对象,包含了要被 saga middleware 执行的信息。redux-saga 为各项任务提供了各种Effects创建器。
- redux-saga启动的任务可以在任何时候通过手动来取消,也可以把任务和其他的Effects放到 race 方法里以自动取消。
安装
$ npm install --save redux-saga
3. 使用示例
假设我们有一个 UI 界面,在单击按钮时从远程服务器获取一些用户数据(为简单起见,我们只列出 action 触发代码)。
class UserComponent extends React.Component {
...
onSomeButtonClicked() {
const { userId, dispatch } = this.props
dispatch({type: 'USER_FETCH_REQUESTED', payload: {userId}})
}
...
}
这个组件 dispatch 一个 plain Object 的 action 到 Store。我们将创建一个 Saga 来监听所有的 USER_FETCH_REQUESTED
action,并触发一个 API 调用获取用户数据。
sagas.js
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'
// worker Saga : 将在 USER_FETCH_REQUESTED action 被 dispatch 时调用
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
/*
在每个 `USER_FETCH_REQUESTED` action 被 dispatch 时调用 fetchUser
允许并发(译注:即同时处理多个相同的 action)
*/
function* mySaga() {
yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}
/*
也可以使用 takeLatest
不允许并发,dispatch 一个 `USER_FETCH_REQUESTED` action 时,
如果在这之前已经有一个 `USER_FETCH_REQUESTED` action 在处理中,
那么处理中的 action 会被取消,只会执行当前的
*/
function* mySaga() {
yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}
export default mySaga;
为了能跑起 Saga,我们需要使用 redux-saga
中间件将 Saga 与 Redux Store 建立连接。
main.js
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import mySaga from './sagas'
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
// then run the saga
sagaMiddleware.run(mySaga)
// render the application
redux-saga基本用法总结:
- 使用 createSagaMiddleware 方法创建 saga 的 Middleware ,然后在创建的 redux 的 store 时,使用 applyMiddleware 函数将创建的 saga Middleware 实例绑定到 store 上,最后可以调用 saga Middleware 的 run 函数来执行某个或者某些 Middleware 。
- 在 saga 的 Middleware 中,可以使用 takeEvery 或者 takeLatest 等 API 来监听某个 action ,当某个 action 触发后, saga 可以使用 call 发起异步操作,操作完成后使用 put 函数触发 action ,同步更新 state ,从而完成整个 State 的更新。
4. 声明式的Effect
在 redux-saga
的世界里,Sagas 都用 Generator 函数实现。我们从 Generator 里 yield 纯 JavaScript 对象以表达 Saga 逻辑。
我们称呼那些对象为 Effect。Effect 是一个简单的对象,这个对象包含了一些给 middleware 解释执行的信息。
你可以把 Effect 看作是发送给 middleware 的指令以执行某些操作(调用某些异步函数,发起一个 action 到 store,等等)。
你可以使用 redux-saga/effects
包里提供的函数来创建 Effect。
redux-saga中最大的特点就是提供了声明式的Effect,声明式的Effect使得redux-saga监听原始js对象形式的action,并且可以方便单元测试。
- 首先,在redux-saga中提供了一系列的api,比如take、put、all、select等API ,在redux-saga中将这一系列的api都定义为Effect。这些Effect执行后,当函数resolve时返回一个描述对象,然后redux-saga中间件根据这个描述对象恢复执行generator中的函数。
首先来看redux-thunk的大体过程:
action1(side function)—>redux-thunk监听—>执行相应的有副作用的方法—>action2(plain object)
转化到action2是一个原始js对象形式的action,然后执行reducer函数就会更新store中的state。
而redux-saga的大体过程如下:
action1(plain object)——>redux-saga监听—>执行相应的Effect方法——>返回描述对象—>恢复执行异步和副作用函数—>action2(plain object)
对比redux-thunk我们发现,redux-saga中监听到了原始js对象action,并不会马上执行副作用操作,会先通过Effect方法将其转化成一个描述对象,然后再将描述对象,作为标识,再恢复执行副作用函数。
通过使用Effect类函数,可以方便单元测试,我们不需要测试副作用函数的返回结果。只需要比较执行Effect方法后返回的描述对象,与我们所期望的描述对象是否相同即可。
举例来说,call方法是一个Effect类方法:
import { call } from 'redux-saga/effects'
function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// ...
}
上述代码中,比如我们需要测试Api.fetch返回的结果是否符合预期,通过调用call方法,返回一个描述对象。这个描述对象包含了所需要调用的方法和执行方法时的实际参数,我们认为只要描述对象相同,也就是说只要调用的方法和执行该方法时的实际参数相同,就认为最后执行的结果肯定是满足预期的,这样可以方便的进行单元测试,不需要模拟Api.fetch函数的具体返回结果。
import { call } from 'redux-saga/effects'
import Api from '...'
const iterator = fetchProducts()
// expects a call instruction
assert.deepEqual(
iterator.next().value,
call(Api.fetch, '/products'),
"fetchProducts should yield an Effect call(Api.fetch, './products')"
)
5. Effect提供的具体方法
引入:
import {take,call,put,select,fork,takeEvery,takeLatest} from 'redux-saga/effects'
1、Saga 辅助函数
redux-saga提供了一些辅助函数,用来在一些特定的action 被发起到Store时派生任务,下面先来讲解两个辅助函数:takeEvery 和 takeLatest。
takeEvery和takeLatest用于监听相应的动作并执行相应的方法,是构建在take和fork上面的高阶api,比如要监听login动作,可以用takeEvery方法:
takeEvery('login',loginFunc)
相当于
while(true){
yield take('login');
yield fork(loginFunc);
}
takeEvery监听到login的动作,就会执行loginFunc方法,除此之外,takeEvery可以同时监听到多个相同的action。
takeLatest方法跟takeEvery是相同方式调用:
takeLatest('login',loginFunc)
与takeLatest不同的是,takeLatest是会监听执行最近的那个被触发的action。如果之前已经有一个任务在执行,那之前的这个任务会自动被取消。
2、Effect Creators
redux-saga框架提供了很多创建effect的函数,下面我们就来简单的介绍下开发中最常用的几种
- take(pattern):在Store上等待指定action
- put(action):向Store发送action
- select(selector, …args):获取Store中的数据
- call(fn, …args):函数调用
- fork(fn, …args):和call类似,但是是非阻塞的,立即返回
如果你想要同时监听不同的action,可以使用all()或者race()把他们组合成一个root saga:
export default function* rootSaga() {
yield all([
takeEvery("FOO_ACTION", fooASaga),
takeEvery("BAR_ACTION", barASaga)
])
}
take(pattern)
take这个方法,是用来监听action,返回的是监听到的action对象。它创建了一个命令对象,告诉middleware等待一个特定的action, Generator会暂停,直到一个与pattern匹配的action被发起,才会继续执行下面的语句,也就是说,take是一个阻塞的 effect 。比如:
const loginAction = {
type:'login'
}
在UI Component中dispatch一个action:
dispatch(loginAction)
在saga中使用:
const action = yield take('login');
可以监听到UI传递到中间件的Action,上述take方法的返回,就是dispath的原始对象。一旦监听到login动作,返回的action为:
{
type:'login'
}
put(action)
在前面提到,redux-saga做为中间件,工作流是这样的:
UI——>action1————>redux-saga中间件————>action2————>reducer…
从工作流中,我们发现redux-saga执行完副作用函数后,必须发出action,然后这个action被reducer监听,从而达到更新state的目的。相应的这里的put对应于redux中的dispatch,工作流程图如下:
从图中可以看出redux-saga执行副作用方法转化action时,put这个Effect方法跟redux原始的dispatch相似,都是可以发出action,且发出的action都会被reducer监听到。put的使用方法:
yield put({type:'login'})
put函数是用来发送action的 effect,你可以简单的把它理解成为redux框架中的dispatch函数,当put一个action后,reducer中就会计算新的state并返回,注意: put 也是阻塞 effect。
select(selector, …args)
put方法与redux中的dispatch相对应,同样的如果我们想在中间件中获取state,那么需要使用select。select方法对应的是redux中的getState,用户获取store中的state,使用方法:
const state= yield select()
call(fn, …args)
call函数你可以把它简单的理解为就是可以调用其他函数的函数,它命令 middleware 来调用fn 函数, args为函数的参数,注意: fn 函数可以是一个 Generator 函数,也可以是一个返回 Promise 的普通函数,call 函数也是阻塞 effect。
yield call(fetch,'/userInfo',username)
fork(fn, …args)
fork 函数和 call 函数很像,都是用来调用其他函数的,但是fork函数是非阻塞函数,也就是说,程序执行完 yield fork(fn, args)
这一行代码后,会立即接着执行下一行代码语句,而不会等待fn函数返回结果后,再执行下面的语句。
export default function* rootSaga() {
// 下面的四个 Generator 函数会一次执行,不会阻塞执行
yield fork(addItemFlow)
yield fork(removeItemFlow)
yield fork(toggleItemFlow)
yield fork(modifyItem)
}