原文连接html
redux-saga 是一个管理 Redux 应用异步操做的中间件,功能相似redux-thunk + async/await
, 它经过建立 Sagas 将全部的异步操做逻辑存放在一个地方进行集中处理。ios
redux-saga中的 Effects 是一个纯文本 JavaScript 对象,包含一些将被 saga middleware 执行的指令。这些指令所执行的操做包括以下三种:git
Effects 中包含的指令有不少,具体能够异步API 参考进行查阅github
assert.deepEqual(iterator.next().value, call(Api.fetch, '/products'))
假如如今有一个场景:用户在登陆的时候须要验证用户的 username 和 password 是否符合要求。redux
获取用户数据的逻辑(user.js):axios
// user.js import request from 'axios'; // define constants // define initial state // export default reducer export const loadUserData = (uid) => async (dispatch) => { try { dispatch({ type: USERDATA_REQUEST }); let { data } = await request.get(`/users/${uid}`); dispatch({ type: USERDATA_SUCCESS, data }); } catch(error) { dispatch({ type: USERDATA_ERROR, error }); } }
验证登陆的逻辑(login.js):swift
import request from 'axios'; import { loadUserData } from './user'; export const login = (user, pass) => async (dispatch) => { try { dispatch({ type: LOGIN_REQUEST }); let { data } = await request.post('/login', { user, pass }); await dispatch(loadUserData(data.uid)); dispatch({ type: LOGIN_SUCCESS, data }); } catch(error) { dispatch({ type: LOGIN_ERROR, error }); } }
异步逻辑能够所有写进 saga.js 中:api
export function* loginSaga() { while(true) { const { user, pass } = yield take(LOGIN_REQUEST) //等待 Store 上指定的 action LOGIN_REQUEST try { let { data } = yield call(loginRequest, { user, pass }); //阻塞,请求后台数据 yield fork(loadUserData, data.uid); //非阻塞执行loadUserData yield put({ type: LOGIN_SUCCESS, data }); //发起一个action,相似于dispatch } catch(error) { yield put({ type: LOGIN_ERROR, error }); } } } export function* loadUserData(uid) { try { yield put({ type: USERDATA_REQUEST }); let { data } = yield call(userRequest, `/users/${uid}`); yield put({ type: USERDATA_SUCCESS, data }); } catch(error) { yield put({ type: USERDATA_ERROR, error }); } }
对于 redux-saga, 仍是有不少比较难以理解和晦涩的地方,下面笔者针对本身以为比较容易混淆的概念进行整理:数组
take 和 takeEvery 都是监听某个 action, 可是二者的做用却不一致,takeEvery 是每次 action 触发的时候都响应,而 take 则是执行流执行到 take 语句时才响应。takeEvery 只是监听 action, 并执行相对应的处理函数,对什么时候执行 action 以及如何响应 action 并无多大的控制权,被调用的任务没法控制什么时候被调用,而且它们也没法控制什么时候中止监听,它只能在每次 action 被匹配时一遍又一遍地被调用。可是 take 能够在 generator 函数中决定什么时候响应一个 action 以及 响应后的后续操做。
例如在监听全部类型的 action 触发时进行 logger 操做,使用 takeEvery 实现以下:app
import { takeEvery } from 'redux-saga' function* watchAndLog(getState) { yield* takeEvery('*', function* logger(action) { //do some logger operation //在回调函数体内 }) }
使用 take 实现以下:
import { take } from 'redux-saga/effects' function* watchAndLog(getState) { while(true) { const action = yield take('*') //do some logger operation //与 take 并行 }) }
其中 while(true)
的意思是一旦到达流程最后一步(logger),经过等待一个新的任意的 action 来启动一个新的迭代(logger 流程)。
call 操做是用来发起异步操做的,对于 generator 来讲,call 是阻塞的操做,它在 Generator 调用结束以前不能执行或处理任何其余事情。,可是 fork 倒是非阻塞操做,当 fork 调动任务时,该任务会在后台执行,此时的执行流能够继续日后面执行而不用等待结果返回。
例如以下的登陆场景:
function* loginFlow() { while(true) { const {user, password} = yield take('LOGIN_REQUEST') const token = yield call(authorize, user, password) if(token) { yield call(Api.storeItem({token})) yield take('LOGOUT') yield call(Api.clearItem('token')) } } }
若在 call 在去请求 authorize 时,结果未返回,可是此时用户又触发了 LOGOUT 的 action,此时的 LOGOUT 将会被忽略而不被处理,由于 loginFlow 在 authorize 中被堵塞了,没有执行到 take('LOGOUT')
那里
如若遇到某个场景须要同一时间执行多个任务,好比 请求 users 数据 和 products 数据, 应该使用以下的方式:
import { call } from 'redux-saga/effects' //同步执行 const [users, products] = yield [ call(fetch, '/users'), call(fetch, '/products') ] //而不是 //顺序执行 const users = yield call(fetch, '/users'), products = yield call(fetch, '/products')
当 yield 后面是一个数组时,那么数组里面的操做将按照 Promise.all
的执行规则来执行,genertor 会阻塞知道全部的 effects 被执行完成
在每个使用 redux-saga 的项目中,主文件中都会有以下一段将 sagas 中间件加入到 Store 的逻辑:
const sagaMiddleware = createSagaMiddleware({sagaMonitor}) const store = createStore( reducer, applyMiddleware(sagaMiddleware) ) sagaMiddleware.run(rootSaga)
其中 createSagaMiddleware 是 redux-saga 核心源码文件 src/middleware.js 中导出的方法:
export default function sagaMiddlewareFactory({ context = {}, ...options } = {}) { ... function sagaMiddleware({ getState, dispatch }) { const channel = stdChannel() channel.put = (options.emitter || identity)(channel.put) sagaMiddleware.run = runSaga.bind(null, { context, channel, dispatch, getState, sagaMonitor, logger, onError, effectMiddlewares, }) return next => action => { if (sagaMonitor && sagaMonitor.actionDispatched) { sagaMonitor.actionDispatched(action) } const result = next(action) // hit reducers channel.put(action) return result } } ... }
这段逻辑主要是执行了 sagaMiddleware()
,该函数里面将 runSaga 赋值给 sagaMiddleware.run 并执行,最后返回 middleware。 接着看 runSaga() 的逻辑:
export function runSaga(options, saga, ...args) { ... const task = proc( iterator, channel, wrapSagaDispatch(dispatch), getState, context, { sagaMonitor, logger, onError, middleware }, effectId, saga.name, ) if (sagaMonitor) { sagaMonitor.effectResolved(effectId, task) } return task }
这个函数里定义了返回了一个 task 对象,该 task 是由 proc 产生的,移步 proc.js:
export default function proc( iterator, stdChannel, dispatch = noop, getState = noop, parentContext = {}, options = {}, parentEffectId = 0, name = 'anonymous', cont, ) { ... const task = newTask(parentEffectId, name, iterator, cont) const mainTask = { name, cancel: cancelMain, isRunning: true } const taskQueue = forkQueue(name, mainTask, end) ... next() return task function next(arg, isErr){ ... if (!result.done) { digestEffect(result.value, parentEffectId, '', next) } ... } }
其中 digestEffect 就执行了 effectTriggerd()
和 runEffect()
,也就是执行 effect,其中 runEffect() 中定义了不一样 effect 执行相对应的函数,每个 effect 函数都在 proc.js 实现了。
除了一些核心方法以外,redux-saga 还提供了一系列的 helper 文件,这些文件的做用是返回一个类 iterator 的对象,便于后续的遍历和执行, 在此不具体分析。