内容简介:本文主要记录了在项目中使用redux-saga的一些总结,如有错误的地方欢迎指正互相学习。redux中的action仅支持原始对象(plain object),处理有副作用的action,需要使用中间件。中间件可以在发出action,到reducer函数接受action之间,执行具有副作用的操作。redux-thunk 和 redux-saga 是 redux 应用中最常用的两种异步流处理方式。
本文主要记录了在项目中使用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使用
注意::warning: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.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后面会讲,总之就是让函数执行获取数据嘛。我们现在需要知道数据流是怎样实现的?
问题1:
“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 入门教程。
问题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 parallel
const [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,因为它们真的很有意思!!
以上~
以上所述就是小编给大家介绍的《记一次redux-saga的项目实践总结》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Approximation Algorithms
Vijay V. Vazirani / Springer / 2001-07-02 / USD 54.95
'This book covers the dominant theoretical approaches to the approximate solution of hard combinatorial optimization and enumeration problems. It contains elegant combinatorial theory, useful and inte......一起来看看 《Approximation Algorithms》 这本书的介绍吧!