本文主要记录了在项目中使用redux-saga的一些总结,若有错误的地方欢迎指正互相学习。html
redux中的action仅支持原始对象(plain object),处理有反作用的action,须要使用中间件。中间件能够在发出action,到reducer函数接受action之间,执行具备反作用的操做。react
redux-thunk 和 redux-saga 是 redux 应用中最经常使用的两种异步流处理方式。git
以前一直使用redux-thunk处理异步等反作用操做,在action中处理异步等反作用操做,此时的action是一个函数,以dispatch,getState做为形参,函数体内的部分能够执行异步。经过redux-thunk来处理异步,action可谓是多种多样,不利于维护。github
redux-thunk
的任务执行方式是从 UI 组件直接触发任务。ajax
redux-thunk
中间件可让action建立函数先不返回一个action对象,而是返回一个函数,函数传递两个参数(dispatch,getState),在函数体内进行业务逻辑的封装express
redux-thunk 的主要思想是扩展 action,使得 action 从一个对象变成一个函数。json
好比下面是一个获取礼品列表的异步操做所对应的actionredux
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
缺点有以下几点:api
action 虽然扩展了,但所以变得复杂,后期可维护性下降;
thunks 内部测试逻辑比较困难,须要mock全部的触发函数;
协调并发任务比较困难,当本身的 action 调用了别人的 action,别人的 action 发生改动,则须要本身主动修改;
业务逻辑会散布在不一样的地方:启动的模块,组件以及thunks内部。
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是经过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 的惟一入口
项目中我是这样用的,若是你有更好的实现方法请分享给我:
在定义生成store的地方,引入并加入redux-sage中间件。
// store/index.js
import { 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__ || compose
const loggerMiddleware = createLogger({ collapsed: true })
// 这是一个能够帮你运行saga的中间件
const sagaMiddleware = createSagaMiddleware()
const store = createStore(reducers,
composeEnhancers(
applyMiddleware(
sagaMiddleware, middlewareRouter, loggerMiddleware
)))
// 经过中间件执行或者说运行saga
sagaMiddleware.run(rootSaga, store)
window.store = store
export 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 saga
function* 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 saga
export 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后面会讲,总之就是让函数执行获取数据嘛。咱们如今须要知道数据流是怎样实现的?
“GETLIST”这个action.type表明的是哪一个函数,这个函数怎么获取到tabs, rule, env这三个参数的?看代码,其实真的很简单。。。
// actions/index.js
export 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的知识,参考:阮一峰Redux 入门教程。
接下来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的使用了:
安装啥的步骤直接略过....
前面说到,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 中应用于不一样的场景,比较经常使用的是:
容许多个请求同时执行,无论以前是否还有一个或多个请求还没有结束。
// 首先咱们建立一个将执行异步 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 辅助函数。
做用同takeEvery同样,惟一的区别是它只关注最后,也就是最近一次发起的异步请求,若是上次请求还未返回,则会被取消。
function* watchFetchData() {
yield takeLatest('FETCH_REQUESTED', fetchData)
}
复制代码
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);
}
复制代码
非阻塞任务调用机制:上面咱们介绍过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
的异步非阻塞特性更适合于在后台运行一些不影响主流程的代码(好比:后台打点/开启监听),这每每是加快页面渲染的一种方式。
做用和 redux 中的 dispatch 相同。
yield put({ type: 'CLICK_BTN' });
复制代码
做用和 redux thunk 中的 getState 相同。
const id = yield select(state => state.id);
复制代码
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})
}
}
复制代码
takeEvery
只是监听每一个 action
,而后执行处理函数。对于合适响应 action
和如何响应 action
, tackEvery
没有权限。
take
只有在执行流达到时才回响应 action
,而 takeEvery
则一经注册,都会响应action
。
all
提供了一种并行执行异步请求的方式。以前介绍过执行异步请求的api中,大都是阻塞执行,只有当一个call操做放回后,才能执行下一个call
操做,call
提供了一种相似Promise
中的all
操做,能够将多个异步操做做为参数参入all
函数中, 若是有一个call
操做失败或者全部call
操做都成功返回,则本次all操做执行完毕。
import { all, call } from 'redux-saga/effects'
// correct, effects will get executed in parallel
const [users, repos] = yield all([
call(fetch, '/users'),
call(fetch, '/repos')
])
复制代码
有时候当咱们并行的发起多个异步操做时,咱们并不必定须要等待全部操做完成,而只须要有一个操做完成就能够继续执行流。这就是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')
})
}
}
复制代码
在以前的操做中,全部的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) { ... } 复制代码
在 saga 中,不管是请求失败,仍是代码异常,都可以经过 try catch 来捕获。
假若访问一个接口出现代码异常,多是网络请求问题,也多是后端数据格式问题,但无论怎样,给予日志上报或友好的错误提示是不可缺乏的,这也每每体现了代码的健壮性,通常会这么作:
function* saga() {
try {
const data = yield call(fetch, '/someEndpoint');
return data;
} catch (error) {
yield put(onError(error));
}
}
复制代码
指的是一种使用两个单独的 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,由于它们真的颇有意思!!
以上~