redux 作为react 的状态管理的时候,副作用处理方案比较多,这里我们讨论 redux-saga 的内容。
需要的准备工作
- react 基础
- redux 基础
- redux 中间基础
- ES6+ 生成器相关知识
常见的 redux 中间件
- redux-logger 中间件
- redux-promise 中间件
- redux-thunk 中间件
- redux-saga 中间件
redux 中间件基础
首先要知道 redux 中间件的作用,其实就是处理副作用,比如:数据层的数据请求,也就是 js 的异步任务。
redux 使用中间件
- 引入 createStore 方法,并创建一个 store:
import { createStore } from 'redux'
const store = createStore(reducer)
- redux 支持中间件, redux 通过暴露出来的 applyMiddleware 方法来支持 redux 中间件。
import { createStore, applyMiddleware } from 'redux'
const store = createStore(reducer, applyMiddleware(/*your middleware*/));
// reducer 是纯函数
- redux-saga 提供了创建中间件的方法
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
redux-saga 中间件的编写
API
redux-saga 对外的暴露 API 可以分为两个部分,这两个部分: 主要部分、副作用部分
主要部分
export { CANCEL, SAGA_LOCATION } from '@redux-saga/symbols'
export { default } from './internal/middleware'
export { runSaga } from './internal/runSaga'
export { END, isEnd, eventChannel, channel, multicastChannel, stdChannel } from './internal/channel'
export { detach } from './internal/io'
import * as buffers from './internal/buffers'
export { buffers }
副作用部分
export {
take,takeMaybe,put,putResolve,all,race,call,apply,cps,fork,spawn,join,
cancel,select,actionChannel,cancelled,flush,getContext,setContext,delay,
} from './internal/io'
export { debounce, retry, takeEvery, takeLatest, takeLeading, throttle } from './internal/io-helpers'
import * as effectTypes from './internal/effectTypes';
export { effectTypes };
从暴露的 api 的位置,我们知道大部分的 redux-saga 的 api 在 packages/core/src/internal 实现。
redux-saga 仅仅是 redux 的中间件,但是我们学着学着就把 redux-saga 理解为 react 的中间件。react 中目前还没有中间件的概念。
saga 文件
我们在写 redux-saga 处理副作用的时候,我们一般是将 saga 处理副作用单独的写在不同的文件中。
在创建了 store 之后,有一件事是特别重要的,sagaMiddleware 还需要运行根 saga。
sagaMiddleware.run(rootSaga)
通过源码我们知道 sagaMiddleware 其实就是 sagaMiddlewareFactory ,这个函数调用创建的是一个函数 sagaMiddleware,sagaMiddleware 挂载了 run 方法和 setContext 方法。
源码:
// ...
function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {}) {
let boundRunSaga
function sagaMiddleware({ getState, dispatch }) {
boundRunSaga = runSaga.bind(null, {
...options,
context,
channel,
dispatch,
getState,
sagaMonitor,
})
return next => action => {
if (sagaMonitor && sagaMonitor.actionDispatched) {
sagaMonitor.actionDispatched(action)
}
const result = next(action) // hit reducers
channel.put(action)
return result
}
}
sagaMiddleware.run = (...args) => {
return boundRunSaga(...args)
}
sagaMiddleware.setContext = props => {
assignWithSymbols(context, props)
}
return sagaMiddleware
}
run 方法是执行了绑定方法boundRunSaga(...args), RunSaga 背后生辰的 task。 也就是说只有执行了 run 方法, redux-saga 才是启动了任务!因为这里面作用特别多处理,我们可以先形而下的理解怎么用。后面在关注原理。
小结
从 redux 中间件简单的使用,和 redux-saga 中间件自生使用的特点区别,理解 redux-saga。
写 saga 文件
写 saga 文件其实,就是将文件异步任务拆分,方便我们管理。
流程
流程种类1:
- 组件派发 action, saga 的 sagaHelper 中 takeEvery 接收到 action.type
- takeEvery 迭代处理后执行副作用函数 effect generator 函数,
redux-saga 接收组件 dispatch 过来的 action
redux-saga 中如何接收组件派发过来的 action 呢?
takeEvery函数调用得到的一个 iterator 对象,看源码:
首先将 action.type(patternOrChannel) 和 副作用函数 worker 单独拿出里,然后重新组装数据: yTake、yFork。调用 fsmIterator 去制作一个迭代器
// takeEvery.js
export default function takeEvery(patternOrChannel, worker, ...args) {
const yTake = { done: false, value: take(patternOrChannel) }
const yFork = ac => ({ done: false, value: fork(worker, ...args, ac) })
let action,
setAction = ac => (action = ac)
return fsmIterator(
{
q1() {
return { nextState: 'q2', effect: yTake, stateUpdater: setAction }
},
q2() {
return { nextState: 'q1', effect: yFork(action) }
},
},
'q1',
`takeEvery(${safeName(patternOrChannel)}, ${worker.name})`,
)
}
- fsmIterator 组装数据制作 iterator 遍历器
export default function fsmIterator(fsm, startState, name) {
let stateUpdater,
errorState,
effect,
nextState = startState
function next(arg, error) {
if (nextState === qEnd) {
return done(arg)
}
if (error && !errorState) {
nextState = qEnd
throw error
} else {
stateUpdater && stateUpdater(arg)
const currentState = error ? fsm[errorState](error) : fsm[nextState]()
;({ nextState, effect, stateUpdater, errorState } = currentState)
return nextState === qEnd ? done(arg) : effect
}
}
return makeIterator(next, error => next(null, error), name)
}
- iterator 内容
export function makeIterator(next, thro = kThrow, name = 'iterator') {
const iterator = { meta: { name }, next, throw: thro, return: kReturn, isSagaIterator: true }
if (typeof Symbol !== 'undefined') {
iterator[Symbol.iterator] = () => iterator
}
return iterator
}
当我们使用 takeEvery 来捕获一个 action.type 时候,就会创建一个迭代器,然后指定副作用 worker 函数。
简单示例
/* eslint-disable no-constant-condition */
import { put, takeEvery, delay } from 'redux-saga/effects'
export function* incrementAsync() {
yield delay(1000)
yield put({ type: 'INCREMENT' })
}
export default function* rootSaga() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
在副作用 wokder 函数中将 action 派发个reducer
我们可以在 wokder effect 函数中执行异步任务,setTimeOut, ajax 请求任务等等...在完成了 effect 后,我们就可以交给 reducer 让ruducer 去计算新的 state, reducer 在将 state 返回给 store, store 中被订阅 state, 会被视图层更新。
put/dispatch
put 就是棒我们将 saga 中间件中的 action 派发给 reducer。 看源码:
export function put(channel, action) {
if (is.undef(action)) {
action = channel
// `undefined` instead of `null` to make default parameter work
channel = undefined
}
return makeEffect(effectTypes.PUT, { channel, action })
}
// 制作一个 effect, 返回了一个 effect 对象
const makeEffect = (type, payload) => ({
[IO]: true,
combinator: false,
type,
payload,
})
import { take, put, call, fork, select } from 'redux-saga/effects'
import { take, put, call, fork, race, cancelled } from 'redux-saga/effects'
import { put, takeEvery, delay } from 'redux-saga/effects'
import { retry, call, put, takeEvery, delay, all, race, fork, spawn, take, select } from 'redux-saga/effects'
import { take, put, call, fork, select, all } from 'redux-saga/effects'
import { take, put, call, fork, select, takeEvery, all } from 'redux-saga/effects'
export default function* root() {
yield fork(startup)
yield fork(nextRedditChange)
yield fork(invalidateReddit)
}
export default function* rootSaga() {
yield fork(watchIncrementAsync)
}
export default function* rootSaga() {
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
export default function* rootSaga() {
yield all([
takeEvery('ACTION_ERROR_IN_PUT', errorInPutSaga),
takeEvery('ACTION_ERROR_IN_SELECT', errorInSelectSaga),
takeEvery('ACTION_ERROR_IN_CALL_SYNC', errorInCallSyncSaga),
takeEvery('ACTION_ERROR_IN_CALL_ASYNC', errorInCallAsyncSaga),
takeEvery('ACTION_ERROR_IN_CALL_INLINE', errorInCallInlineSaga),
takeEvery('ACTION_ERROR_IN_FORK', errorInForkSaga),
takeEvery('ACTION_ERROR_IN_SPAWN', errorInSpawnSaga),
takeEvery('ACTION_ERROR_IN_RACE', errorInRaceSaga),
takeEvery('ACTION_CAUGHT_ERROR', caughtErrorSaga),
fork(function* inlinedSagaName() {
while (true) {
yield take('ACTION_INLINE_SAGA_ERROR')
yield call(throwAnErrorSaga)
}
}),
takeEvery('ACTION_IN_DELEGATE_ERROR', errorInDelegateSaga),
takeEvery('ACTION_FUNCTION_EXPRESSION_ERROR', funcExpressionSaga),
takeEvery('ACTION_ERROR_IN_RETRY', errorInRetrySaga),
takeEvery('ACTION_ERROR_PRIMITIVE', primitiveErrorSaga),
])
}
export default function* root() {
yield all([
fork(watchNavigate),
fork(watchLoadUserPage),
fork(watchLoadRepoPage),
fork(watchLoadMoreStarred),
fork(watchLoadMoreStargazers)
])
}
export default function* root() {
yield all([fork(getAllProducts), fork(watchGetProducts), fork(watchCheckout)])
}
学习更新中...