Redux-saga is another side effect model for Redux applications. Can be used to replace the Redux-Thunk middleware. Redux-saga abstracts out effects (effects, such as waiting for action, issuing action, fetch data, and so on) for easy composition and testing.
I want to take a look at redux-thunk before I go into redux-saga. Redux-thunk is linked in my previous article. Let’s write an asyncTodo demo using redux-thunk
Story – thunk analysis
import { createStore, applyMiddleware } from 'redux';
const thunk = ({ dispatch, getState }) = > next => action= > {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
const logger = ({ getState }) = > next => action= > {
console.log('will dispatch', getState());
next(action)
console.log('state after dispatch', getState());
}
const todos = (state = [], action) = > {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
action.text
];
default:
return state
}
}
const store = createStore(
todos,
['Use Redux'],
applyMiddleware(logger, thunk),
);
store.dispatch(dispatch= > {
setTimeout((a)= > {
dispatch({ type: 'ADD_TODO'.text: 'Read the docs' });
}, 1000);
});
Copy the code
Redux-thunk allows action to be function instead of plain object. When we want to throw an asynchronous action, we are actually putting the asynchronous processing in actionCreator. As a result, the action forms are not uniform, and the asynchronous processing will be scattered among the actions, which is not conducive to maintenance. How is Redux-Saga implemented
redux-saga
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { put, take, fork, delay } from './redux-saga/effects'
import { delay as delayUtil } from 'redux-saga/utils';
// Get the redux middleware
const sagaMiddleware = createSagaMiddleware({
sagaMonitor: {
// Print effect to analyze redux-saga behavior
effectTriggered(options) {
console.log(options); }}})function* rootSaga() {
const action = yield take('ADD_TODO_SAGA');
// delay(): { type: 'call', payload: { args: [1000], fn }}
yield delay(1000); // or yield call(delayUtil, 1000)
// put(): { type: 'PUT', payload: { action: {}, channel: null }}
yield put({ type: 'ADD_TODO'.text: action.text });
}
const store = createStore(
todos,
['Use Redux'],
applyMiddleware(logger, sagaMiddleware),
);
/ / start of saga
sagaMiddleware.run(rootSaga);
store.dispatch({ type: 'ADD_TODO_SAGA'.text: 'Use Redux-saga' });
Copy the code
As you can see, this is a pure action, and saga listens for the ADD_TODO_SAGA event after it starts, and executes subsequent code if the event occurs.
The source code
stdChannel
Before we start createSagaMiddleware, let’s take a look at channel Redux-Saga. The redux-Saga receives and emits actions to exchange data with the outside world. There are three channels in Redux-Saga. Channel, eventChannel, multicastChannel; Here we’ll just examine the most used multicastChannel
export function multicastChannel() {
let closed = false
// it's currentTakers and nextTakers for the same reason that redux subscribes to prevent taker from changing around taker.
let currentTakers = []
let nextTakers = currentTakers
const ensureCanMutateNextTakers = (a)= > {
if(nextTakers ! == currentTakers) {return
}
nextTakers = currentTakers.slice()
}
const close = (a)= > {
closed = true
const takers = (currentTakers = nextTakers)
for (let i = 0; i < takers.length; i++) {
const taker = takers[i]
taker(END)
}
nextTakers = []
}
return {
[MULTICAST]: true,
put(input) {
if (closed) {
return
}
if (isEnd(input)) {
close()
return
}
const takers = (currentTakers = nextTakers)
// Go through takers, find taker that matches input and execute it.
for (let i = 0; i < takers.length; i++) {
const taker = takers[i]
if (taker[MATCH](input)) {
taker.cancel()
taker(input)
}
}
},
// Save callback and configure the function
take(cb, matcher = matchers.wildcard) {
if (closed) {
cb(END)
return
}
cb[MATCH] = matcher
ensureCanMutateNextTakers()
nextTakers.push(cb)
cb.cancel = once((a)= > {
ensureCanMutateNextTakers()
remove(nextTakers, cb)
})
},
close,
}
}
export function stdChannel() {
const chan = multicastChannel()
const { put } = chan
chan.put = input= > {
if (input[SAGA_ACTION]) {
put(input)
return
}
// For the time being
asap((a)= > put(input))
}
return chan
}
Copy the code
createSagaMiddleware
Get Redux-Middleware and initialize the runsaga function with the parameters needed to start Saga Bind later
export default function sagaMiddlewareFactory({ context = {}, ... options } = {}) {
const { sagaMonitor, logger, onError, effectMiddlewares } = options
let boundRunSaga
// redux middleware
function sagaMiddleware({ getState, dispatch }) {
// Create a channel
const channel = stdChannel()
channel.put = (options.emitter || identity)(channel.put)
boundRunSaga = 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
// Pass events to Saga
channel.put(action)
return result
}
}
/ / start of saga
sagaMiddleware.run = (. args) = > {
// ...
returnboundRunSaga(... args) }/ /...
return sagaMiddleware
}
Copy the code
runsaga
export function runSaga(options, saga, ... args) {
// generate iterator
constiterator = saga(... args)const {
channel = stdChannel(),
dispatch,
getState,
context = {},
sagaMonitor,
logger,
effectMiddlewares,
onError,
} = options
const effectId = nextSagaId()
// Some error checking
// ...
const log = logger || _log
const logError = err= > {
log('error', err)
if (err && err.sagaStack) {
log('error', err.sagaStack)
}
}
constmiddleware = effectMiddlewares && compose(... effectMiddlewares)FinalizeRunEffect => runEffect
const finalizeRunEffect = runEffect= > {
if (is.func(middleware)) {
return function finalRunEffect(effect, effectId, currCb) {
const plainRunEffect = eff= > runEffect(eff, effectId, currCb)
return middleware(plainRunEffect)(effect)
}
} else {
return runEffect
}
}
const env = {
stdChannel: channel,
dispatch: wrapSagaDispatch(dispatch),
getState,
sagaMonitor,
logError,
onError,
finalizeRunEffect,
}
// Create a task to control the Generator process, similar to automatic process management, which will be covered later
const task = proc(env, iterator, context, effectId, getMetaInfo(saga), null)
return task
}
Copy the code
The core of Redux-Saga is task, which controls the flow of the generator function Saga. Is a complex automatic process management, let’s look at a simple automatic process management
// A delay function that returns promise
const delay = (ms) = > {
return new Promise((res) = > {
setTimeout(res, ms);
});
}
function *main() {
yield delay(1000);
console.log('1s later');
yield delay(2000);
console.log('done');
}
// In order to achieve the desired results, we must execute the next statement after the Promise resolved, for example
const gen = main();
const r1 = gen.next();
r1.value.then((a)= > {
const r2 = gen.next();
r2.value.then((a)= >{ gen.next(); })})Copy the code
Using recursive implementation, automatic flow control
function autoRun(gfunc) {
const gen = gfunc();
function next() {
const res = gen.next();
if (res.done) return;
res.value.then(next);
}
next();
}
autoRun(main);
Copy the code
The automatic flow control function above only supports Promise.
proc
export default function proc(env, iterator, parentContext, parentEffectId, meta, cont) {
// ...
const task = newTask(parentEffectId, meta, cont)
const mainTask = { meta, cancel: cancelMain, _isRunning: true._isCancelled: false }
// Build the Task tree
const taskQueue = forkQueue(
mainTask,
function onAbort() { cancelledDueToErrorTasks.push(... taskQueue.getTaskNames()) }, end, ) next()// then return the task descriptor to the caller
return task
function next(arg, isErr) {
let result
if (isErr) {
result = iterator.throw(arg)
} else if (shouldCancel(arg)) {
// ...
} else if (shouldTerminate(arg)) {
// ...
} else {
result = iterator.next(arg)
}
if(! result.done) {// If not, effect is executed
digestEffect(result.value, parentEffectId, ' ', next)
} else {
/** This Generator has ended, terminate the main task and notify the fork queue **/
mainTask._isRunning = false
mainTask.cont(result.value)
}
}
function digestEffect(effect, parentEffectId, label = ' ', cb) {
// Encapsulates cb functions and adds event hooks
function currCb(res, isErr) {
if (effectSettled) {
return
}
effectSettled = true
cb.cancel = noop // defensive measure
if (env.sagaMonitor) {
if (isErr) {
env.sagaMonitor.effectRejected(effectId, res)
} else {
env.sagaMonitor.effectResolved(effectId, res)
}
}
if (isErr) {
crashedEffect = effect
}
cb(res, isErr)
}
runEffect(effect, effectId, currCb)
}
// The function that executes each effect
function runEffect(effect, effectId, currCb) {
if (is.promise(effect)) {
resolvePromise(effect, currCb)
} else if (is.iterator(effect)) {
resolveIterator(effect, effectId, meta, currCb)
} else if (effect && effect[IO]) {
const { type, payload } = effect
if (type === effectTypes.TAKE) runTakeEffect(payload, currCb)
else if (type === effectTypes.PUT) runPutEffect(payload, currCb)
else if (type === effectTypes.CALL) runCallEffect(payload, effectId, currCb)
// All other effects...
else currCb(effect)
} else {
// anything else returned as is
currCb(effect)
}
}
// When the return value is promise, just like the automatic process control function implemented earlier
function resolvePromise(promise, cb) {
// ...
promise.then(cb, error => cb(error, true))}// When it is a generator function
function resolveIterator(iterator, effectId, meta, cb) {
proc(env, iterator, taskContext, effectId, meta, cb)
}
// If a match occurs, the callback will be triggered
function runTakeEffect({ channel = env.stdChannel, pattern, maybe }, cb) {
const takeCb = input= > {
if (input instanceof Error) {
cb(input, true)
return
}
if(isEnd(input) && ! maybe) { cb(TERMINATE)return
}
cb(input)
}
try {
channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null)}catch (err) {
cb(err, true)
return
}
cb.cancel = takeCb.cancel
}
function runPutEffect({ channel, action, resolve }, cb) {
asap((a)= > {
let result
try {
/ / send the action
result = (channel ? channel.put : env.dispatch)(action)
} catch (error) {
cb(error, true)
return
}
if (resolve && is.promise(result)) {
resolvePromise(result, cb)
} else {
cb(result)
}
})
// Put cannot be cancelled
}
function runCallEffect({ context, fn, args }, effectId, cb) {
let result
try {
result = fn.apply(context, args)
} catch (error) {
cb(error, true)
return
}
return is.promise(result)
? resolvePromise(result, cb)
: is.iterator(result)
? resolveIterator(result, effectId, getMetaInfo(fn), cb)
: cb(result)
}
}
Copy the code
conclusion
Redux-saga abstracts the asynchronous operation as effect and uses generator functions to control the saga process. So far, I’ve only covered some basic processes, which will be supplemented in the next article.