前言
本文主要记录了在项目中使用redux-saga的一些总结,如有错误的地方欢迎指正互相学习。
redux中的action仅支持原始对象(plain object),处理有副作用的action,需要使用中间件。中间件可以在发出action,到reducer函数接受action之间,执行具有副作用的操作。
redux-thunk 和 redux-saga 是 redux 应用中最常用的两种异步流处理方式。
之前一直使用redux-thunk处理异步等副作用操作,在action中处理异步等副作用操作,此时的action是一个函数,以dispatch,getState作为形参,函数体内的部分可以执行异步。通过redux-thunk来处理异步,action可谓是多种多样,不利于维护。
redux-thunk
redux-thunk简单介绍
redux-thunk
的任务执行方式是从 UI 组件直接触发任务。
redux-thunk
中间件可以让action创建函数先不返回一个action对象,而是返回一个函数,函数传递两个参数(dispatch,getState),在函数体内进行业务逻辑的封装
redux-thunk 的主要思想是扩展 action,使得 action 从一个对象变成一个函数。
redux-thunk使用
比如下面是一个获取礼品列表的异步操作所对应的action
export default () => dispatch => { fetch('/api/goodList', { // fecth返回的是一个promise method: 'get', dataType: 'json' }).then( json => { var json = JSON.parse(json) if (json.code === 200) { dispatch({ type: 'init', data: json.data }) } }, error => { console.log(error) } )}复制代码
从这个具有副作用的action中,我们可以看出,函数内部极为复杂。如果需要为每一个异步操作都如此定义一个action,显然action不易维护。
redux-thunk
缺点
总结一下redux-thunk
缺点有如下几点:
-
action 虽然扩展了,但因此变得复杂,后期可维护性降低;
-
thunks 内部测试逻辑比较困难,需要mock所有的触发函数;
-
协调并发任务比较困难,当自己的 action 调用了别人的 action,别人的 action 发生改动,则需要自己主动修改;
-
业务逻辑会散布在不同的地方:启动的模块,组件以及thunks内部。
redux-saga
redux-saga简单介绍
redux-saga
中是这样介绍的:
redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。
刚开始了解Saga时,看官方解释,并不是很清楚到底是什么?Saga的副作用(side effects)到底是什么?
通读了官方文档后,大概了解到,副作用就是在action触发reduser之后执行的一些动作, 这些动作包括但不限于,连接网络,io读写,触发其他action。并且,因为Sage的副作用是通过redux的action触发的,每一个action,sage都会像reduser一样接收到。并且通过触发不同的action, 我们可以控制这些副作用的状态, 例如,启动,停止,取消。
所以,我们可以理解为Sage是一个可以用来处理复杂的异步逻辑的模块,并且由redux的action触发。
saga特点:
1.saga的应用场景是复杂异步,如长时事务LLT(long live.transcation)等业务场景。2.方便测试,可以使用takeEvery打印logger。3.提供takeLatest/takeEvery/throttle方法,可以便利的实现对事件的仅关注最近事件、关注每一次、事件限频4.提供cancel/delay方法,可以便利的取消、延迟异步请求5.提供race(effects),[…effects]方法来支持竞态和并行场景6.提供channel机制支持外部事件复制代码
Redux Saga适用于对事件操作有细粒度需求的场景,同时他们也提供了更好的可测试性。
redux-saga使用
注意:⚠️redux-saga是通过ES6中的generator实现的(babel的基础版本不包含generator语法,因此需要在使用saga的地方import ‘babel-polyfill’)。
redux-saga本质是一个可以自执行的generator。
在 redux-saga 中,UI 组件自身从来不会触发任务,它们总是会 dispatch 一个 action 来通知在 UI 中哪些地方发生了改变,而不需要对 action 进行修改。redux-saga 将异步任务进行了集中处理,且方便测试。
所有的东西都必须被封装在 sagas 中。sagas 包含3个部分,用于联合执行任务:
worker saga
(1)做所有的工作,如调用 API,进行异步请求,并且获得返回结果
watcher saga
(2)监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务
(3)root saga
立即启动 sagas 的唯一入口
项目中我是这样用的,如果你有更好的实现方法请分享给我:
给redux添加中间件
在定义生成store的地方,引入并加入redux-sage中间件。
// store/index.jsimport { createStore, applyMiddleware, compose } from 'redux'import { routerMiddleware } from 'react-router-redux'import createSagaMiddleware from 'redux-saga'import createHistory from 'history/createHashHistory'import { createLogger } from 'redux-logger'import { rootSaga } from '../rootSaga'import reducers from '../reducers/saga-reducer'const history = createHistory()const middlewareRouter = routerMiddleware(history)const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || composeconst loggerMiddleware = createLogger({ collapsed: true })// 这是一个可以帮你运行saga的中间件const sagaMiddleware = createSagaMiddleware()const store = createStore(reducers, composeEnhancers( applyMiddleware( sagaMiddleware, middlewareRouter, loggerMiddleware )))// 通过中间件执行或者说运行sagasagaMiddleware.run(rootSaga, store)window.store = storeexport default store复制代码
说明:程序启动时,run(rootSaga) 会开启 sagaMiddleware 对某些 action 进行监听,当后续程序中有触发 dispatch(action) (比如:用户点击)的时候,由于数据流会经过 sagaMiddleware,所以 sagaMiddleware 能够判断当前 action 是否有被监听?如果有,就会进行相应的操作(比如:发送一个异步请求);如果没有,则什么都不做。
// rootSaga.js// 处理浏览器兼容问题import 'babel-polyfill'import { all,call } from 'redux-saga/effects'import { lotterySagaRoot } from './components'import { getchampionListFlow, getTabsListFlow } from './container'export function* rootSaga () { yield all([call(getTabsListFlow), call(getchampionListFlow), call(lotterySagaRoot), ])}复制代码
rootSaga
是我们实际发送给Redux中间件的。
rootSaga
在应用程序启动时被触发一次,可以被认为是在后台运行的进程,监视着所有的动作派发到仓库(store)。
我们单拿出一个 getTabsListFlow 这个saga来进行讲解究竟发生了什么?
写到这里有必要说一下业务逻辑了,getTabsListFlow这个函数是一个watcher saga
,它 watch 的谁呢?getTabsList
这个worker saga
函数,废话不多说看代码:
// 处理浏览器兼容问题import 'babel-polyfill'import { call, put, take, fork } from 'redux-saga/effects'import * as types from '../../action_type'import { lists } from '../../actions/server'const { GETLIST, TABS_UPDATE, START_FETCH, FETCH_ERROR, FETCH_END } = types//----worker sagafunction* getTabsList (tabs, rule, env) { yield put({ type: START_FETCH }) try { return yield call(lists, tabs, rule, env) } catch (err) { yield put({ type: FETCH_ERROR,err}) } finally { yield put({ type: FETCH_END }) }}//-----watcher sagaexport default function* getTabsListFlow() { while (true) { const { tabs, rule, env } = yield take(GETLIST) const { code, data } = yield call(getTabsList, tabs, rule, env) yield put({ type: TABS_UPDATE, data, code }) }}复制代码
上面的代码可以看到,getTabsListFlow这个函数响应一个action,“GETLIST”,获取tabs, rule, env这三个参数传给,getTabsList,这个函数,然后把获取到的结果通过响应一个TABS_UPDATE这个action.type给reducer去出更新数据到页面。
那么这些call, put, take, fork这些API后面会讲,总之就是让函数执行获取数据嘛。我们现在需要知道数据流是怎样实现的?
问题1:
“GETLIST”这个action.type代表的是哪个函数,这个函数怎么获取到tabs, rule, env这三个参数的?看代码,其实真的很简单。。。
// actions/index.jsexport function getList(tabs, rule, env) { return { type: GETLIST, tabs, rule, env, }}复制代码
看到没有我导出了这样一个函数,给了它一个action.type就是叫GETLIST, yield take(GETLIST)就是让这个函数执行了,这三个参数也是这样传递进来的,我只需要在页面上引入这个函数去让个函数执行并传递参数就行了。
import React, { Component } from 'react'import { bindActionCreators } from 'redux'import { Link } from 'react-router-dom'import PropTypes from 'prop-types'import { connect } from 'react-redux'import { getList } from '../../actions/index'class List extends Component { state = { tabs: 'anchor', rule: 'hour', active: 'anchor', hover: 'allanchor', visible: false, } componentDidMount() { const { tabs, rule } = this.state this.props.getList(tabs, rule, env) } ....省略一些代码 List.propTypes = { getList: PropTypes.func}function mapStateToProps(state) { return { ...state, }}function mapDispatchToProps(dispatch) { return { getList: bindActionCreators(getList, dispatch), }}export default connect( mapStateToProps, mapDispatchToProps)(List)复制代码
这样的话"const { tabs, rule, env } = yield take(GETLIST)
"这一段代码就获取到我传递的参数了。
这里设计到了redux的知识,参考:阮一峰。
问题2:
接下来yield call(getTabsList, tabs, rule, env)
,让getTabsList执行,里面发了一个请求lists执行并传递参数。
lists是什么?其实它就是一个异步请求。
/** * 排行榜 * * @param {String} type * @param {String} rule * @return {Promise} */export const lists = (type, rule) => req({ endpoint: `${APP_NAME}/data/${type}/${rule}/${env}`, method: GET,})复制代码
这个是一个被封装好的fectch请求。类似于这样
// 通过fetch获取百度的错误提示页面fetch('https://www.baidu.com/search/error.html?a=1&b=2', { // 在URL中写上传递的参数 method: 'GET' }) .then((res)=>{ return res.text() }) .then((res)=>{ console.log(res) })复制代码
接下来执行到这里 const { code, data } = yield call(getTabsList, tabs, rule, env)
yield put({ type: TABS_UPDATE, data, code })
,到这里我们已经通过请求获取到我们想要的数据了,下一步就是去reducer里生成新的state了。
const userReducer = (state = defaultState, action = {}) => { const { type} = action; switch (type) { case TABS_UPDATE: return Object.assign({}, state, { list: action.data, loading: false }) default: return state; }};复制代码
总结一下:
(1)引入的 redux-saga/effects
都是纯函数,每个函数构造一个特殊的对象,其中包含着中间件需要执行的指令,如:call(lists, tabs, rule, env) 返回一个类似于 {type: CALL, function: lists, args: [tabs, rule, env]} 的对象。
(2)在 watcher saga getTabsListFlow
中:
首先 yield take(GETLIST)
来告诉中间件我们正在等待一个类型为 GETLIST
的 action,然后中间件会暂停执行 getTabsListFlow generator 函数,直到 GETLIST
action(getList) 被 dispatch。一旦我们获得了匹配的 action,中间件就会恢复执行 generator 函数。
下一条指令 const { code, data } = yield call(getTabsList, tabs, rule, env)
告诉中间件去执行getTabsList,并把{tabs, rule, env} 作为 getTabsList 函数的参数传递。中间件会触发 getTabsList generator。
(3)在 worker saga getTabsList
中, yield call(lists, tabs, rule, env)
指示中间件去调用 fetch
函数,同时,会阻塞getTabsList 的执行,中间件会停止 generator
函数,直到 fetch
返回的 Promise
被 resolved
(或 rejected
),然后才恢复执行 generator
函数。
借一张基于 redux-saga 的一次 完整单向数据流单项数据流的图:
到此为止就是我在项目中使用redux-saga针对于其中一个请求来实现的数据处理。
下面开始介绍一些API的使用了:
redux-saga API
安装啥的步骤直接略过....
Effects
前面说到,saga 是一个 generator function,这就意味着它的执行原理必然是下面这样:
function isPromise(value) { return value && typeof value.then === 'function';}const iterator = saga(/* ...args */);// 方法一:// 一步一步,手动执行let result;result = iterator.next();result = iterator.next(result.value);result = iterator.next(result.value);// ...// done!!// 方法二:// 函数封装,自主执行function next(args) { const result = iterator.next(args); if (result.done) { // 执行结束 console.log(result.value); } else { // 根据 yielded 的值,决定什么时候继续执行(resume) if (isPromise(result.value)) { result.value.then(next); } else { next(result.value) } }}next();复制代码
也就是说,generator function
在未执行完前(即:result.done === false
),它的控制权始终掌握在 执行者(caller)手中,即:
-
caller 决定什么时候 恢复(resume)执行。
-
caller 决定每次 yield expression 的返回值。
而 caller 本身要实现上面上述功能需要依赖原生 API :iterator.next(value) ,value 就是 yield expression 的返回值。
举个例子:
function* hello() { const value = yield Promise.reslove('hello saga'); console.log('value: ', value); // value??}复制代码
单纯的看 hello 函数,没人知道 value 的值会是多少?
这完全取决于 gen 的执行者(caller),如果使用上面的 next 方法来执行它,value 的值就是 'hello saga',因为 next 方法对 expression 为 promise 时,做了特殊处理(这不就是缩小版的 co 么~ wow~⊙o⊙)。
换句话说,expression 可以是任何值,关键是 caller 如何来解释 expression,并返回合理的值 !
以此结论,推理来看:
大家熟知的 co 可以认为是一个 caller,它解释的 expression 是:promise/thunk/generator function/iterator 等。
这里的 sagaMiddleware 也算是一个 caller,它主要解释的 expression 就是 effect(当然还可以是 promise/iterator) 。
讲了这么多,那么 effect 到底是什么呢?先来看看官方解释:
An effect is a plain JavaScript Object containing some instructions to be executed by the saga middleware.
意思是说:effect 本质上是一个普通对象,包含着一些指令信息,这些指令最终会被 saga middleware 解释并执行。
用一段代码来解释上述这句话:
function* fetchData() { // 1. 创建 effect const effect = call(ajax.get, '/userLogin'); console.log('effect: ', effect); // effect: // { // CALL: { // context: null, // args: ['/userLogin'], // fn: ajax.get, // } // } // 2. 执行 effect,即:调用 ajax.get('/userLogin') const value = yield effect; console.log('value: ', value);}复制代码
可以明显的看出:
call 方法用来创建 effect 对象,被称作是 effect factory。
yield 语法将 effect 对象 传给 sagaMiddleware,被解释执行,并返回值。
这里的 call effect 表示执行 ajax.get('user/Login')
,又因为它的返回值是 promise
, 为了等待异步结果返回,fetchData
函数会暂时处于 阻塞 状态。
除了上述所说的 call effect 之外,redux-saga 还提供了很多其他 effect 类型,它们都是由对应的 effect factory 生成,在 saga 中应用于不同的场景,比较常用的是:
takeEvery
允许多个请求同时执行,不管之前是否还有一个或多个请求尚未结束。
// 首先我们创建一个将执行异步 action 的任务:import { call, put,takeEvery } from 'redux-saga/effects'export function* fetchData(action) { try { const data = yield call(Api.fetchUser, action.payload.url); yield put({ type: "FETCH_SUCCEEDED", data}); } catch (error) { yield put({ type: "FETCH_FAILED", error}); }}//然后在每次 FETCH_REQUESTED action 被发起时启动上面的任务。function* watchFetchData() { yield* takeEvery('FETCH_REQUESTED', fetchData)}复制代码
在上面的例子中,takeEvery 允许多个 fetchData 实例同时启动。在某个特定时刻,尽管之前还有一个或多个 fetchData 尚未结束,我们还是可以启动一个新的 fetchData 任务,
如果我们只想得到最新那个请求的响应(例如,始终显示最新版本的数据)。我们可以使用 takeLatest 辅助函数。
takeLatest
作用同takeEvery一样,唯一的区别是它只关注最后,也就是最近一次发起的异步请求,如果上次请求还未返回,则会被取消。
function* watchFetchData() { yield takeLatest('FETCH_REQUESTED', fetchData)}复制代码
call
all用来调用异步函数,将异步函数和函数参数作为call函数的参数传入,返回一个js对象。saga引入他的主要作用是方便测试,同时也能让我们的代码更加规范化。
同js原生的call一样,call函数也可以指定this对象,只要把this对象当第一个参数传入call方法就好了
saga同样提供apply函数,作用同call一样,参数形式同js原生apply方法。
// 模拟数据异步获取function fn() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('hello saga'); }, 2000); });}function* fetchData() { // 等待 2 秒后,打印欢迎语(阻塞) const greeting = yield call(fn); console.log('greeting: ', greeting); }复制代码
fork
非阻塞任务调用机制:上面我们介绍过call
可以用来发起异步操作,但是相对于 generator
函数来说,call
操作是阻塞的,只有等 promise
回来后才能继续执行,而fork是非阻塞的 ,当调用 fork
启动一个任务时,该任务在后台继续执行,从而使得我们的执行流能继续往下执行而不必一定要等待返回。
还是上面的栗子:
// 模拟数据异步获取function fn() { return new Promise((resolve, reject) => { setTimeout(() => { resolve('hello saga'); }, 2000); });}function* fetchData() { // 立即打印 task 对象(非阻塞) const task = yield fork(fn); console.log('task: ', task);}复制代码
显然,fork
的异步非阻塞特性更适合于在后台运行一些不影响主流程的代码(比如:后台打点/开启监听),这往往是加快页面渲染的一种方式。
put
作用和 redux 中的 dispatch 相同。
yield put({ type: 'CLICK_BTN' });复制代码
select
作用和 redux thunk 中的 getState 相同。
const id = yield select(state => state.id);复制代码
take
take(pattern)
用以下规则来解释 pattern:
1.如果调用 take 时参数为空,或者传入 '*',那将会匹配所有发起的 action(例如,take() 会匹配所有的 action)。2.如果是一个函数,action 会在 pattern(action) 返回为 true 时被匹配(例如,take(action => action.entities) 会匹配那些 entities 字段为真的 action)。3.如果是一个字符串,action 会在 action.type === pattern 时被匹配(例如,take(INCREMENT_ASYNC))。4.如果参数是一个数组,会针对数组所有项,匹配与 action.type 相等的 action(例如,take([INCREMENT, DECREMENT]) 会匹配 INCREMENT 或 DECREMENT 类型的 action)。复制代码
当在generator
中使用 take
语句等待 action
时, generator
被阻塞,等待 action
被分发,然后继续往下执行,有种 Event.once() 事件监听的感觉。
export function* getAdDataFlow() { while (true){ let request = yield take(homeActionTypes.GET_AD); let response = yield call(getAdData,request.url); yield put({ type:homeActionTypes.GET_AD_RESULT_DATA,data:response.data}) }}复制代码
take VS tackEvery
takeEvery
只是监听每个 action
,然后执行处理函数。对于合适响应 action
和如何响应 action
, tackEvery
没有权限。
最大的区别:
take
只有在执行流达到时才回响应 action
,而 takeEvery
则一经注册,都会响应action
。
all
all
提供了一种并行执行异步请求的方式。之前介绍过执行异步请求的api中,大都是阻塞执行,只有当一个call操作放回后,才能执行下一个call
操作,call
提供了一种类似Promise
中的all
操作,可以将多个异步操作作为参数参入all
函数中, 如果有一个call
操作失败或者所有call
操作都成功返回,则本次all操作执行完毕。
import { all, call } from 'redux-saga/effects' // correct, effects will get executed in parallelconst [users, repos] = yield all([ call(fetch, '/users'), call(fetch, '/repos')])复制代码
race
有时候当我们并行的发起多个异步操作时,我们并不一定需要等待所有操作完成,而只需要有一个操作完成就可以继续执行流。这就是race
的用处。
他可以并行的启动多个异步请求,只要有一个 请求返回(resolved或者reject),race
操作接受正常返回的请求,并且将剩余的请求取消。
import { race, take, put } from 'redux-saga/effects' function* backgroundTask() { while (true) { ... }} function* watchStartBackgroundTask() { while (true) { yield take('START_BACKGROUND_TASK') yield race({ task: call(backgroundTask), cancel: take('CANCEL_TASK') }) }}复制代码
actionChannel
在之前的操作中,所有的action
分发是顺序的,但是对action
的响应是由异步任务来完成,也即是说对action的处理是无序的。
如果需要对action
的有序处理的话,可以使用actionChannel
函数来创建一个action
的缓存队列,但一个action
的任务流程处理完成后,才可是执行下一个任务流。
import { take, actionChannel, call, ... } from 'redux-saga/effects' function* watchRequests() { // 1- Create a channel for request actions const requestChan = yield actionChannel('REQUEST') while (true) { // 2- take from the channel const {payload} = yield take(requestChan) // 3- Note that we're using a blocking call yield call(handleRequest, payload) }} function* handleRequest(payload) { ... }复制代码
Error Handling
在 saga 中,无论是请求失败,还是代码异常,均可以通过 try catch 来捕获。
倘若访问一个接口出现代码异常,可能是网络请求问题,也可能是后端数据格式问题,但不管怎样,给予日志上报或友好的错误提示是不可缺少的,这也往往体现了代码的健壮性,一般会这么做:
function* saga() { try { const data = yield call(fetch, '/someEndpoint'); return data; } catch (error) { yield put(onError(error)); }}复制代码
Watcher/Worker
指的是一种使用两个单独的 Saga
来组织控制流的方式。
Watcher: 监听发起的 action
并在每次接收到 action
时 fork
一个 worker
。
Worker: 处理 action
并结束它。
function* watcher() { while(true) { const action = yield take(ACTION) yield fork(worker, action.payload) }}function* worker(payload) { // ... do some stuff}复制代码
事实上因为项目的局限性很多API并没有用上,可以根据项目的实际需求使用这些API,因为它们真的很有意思!!
以上~
参考
文章:
-
-