The upper part is the English version, and the lower part is the Chinese version, with the same content. If there are any wrong, or you have anything hard to understand, pls feel free to let me know.many thx.
Overview
From The data flow, How to use, Debug the source code, The principle analysis of the core API, How to expand your own saga, and Further analyze the source code of key functions to analyze redux-saga in detail from 6 angles
1.The data flow
2 How to use
//register saga
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(toDoApp, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
// every saga is a generator
import { all, call, put, takeEvery } from 'redux-saga/effects';
import { TODO_LIST } from '../actions';
//request data, then go to dispatch action by put
export function* fetchToDoList() {
const data = yield call(fetch, url);
yield put({ type: TODO_LIST, toDoList: data });
}
//Listen saga
export function* loadToDoList() {
yield takeEvery(LOAD_TODO_LIST, fetchToDoList);
}
//Listen to all saga in parallel
export default function* rootSaga() {
yield all([loadToDoList()]);
}
Note: redux-saga does not intercept action, but captures action by listening. Because it uses channel to cache effect runner, and continues to execute next(). So the original action will still go to reducer
3 Debug the source code
3.1 How to debug the source code
//1. Go into redux-saga/packages/redux-saga and execute the following command
yarn link
//2. Go into your project and execute the following command
yarn link redux-saga
Note: if you changed the source code, the corresponding code needs to be compiled. for example If the core source code is modified, redux-saga-effects.esm.js needs to be recompiled
Or directly modify the code of node_module and debug by the chrome devtool breakpoint
3.2 Debug the execution flow of saga in chrome devtool
3.3.1 Entrance: store.js
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
Use a simple saga project example to debug, you can use any project that contains saga, because the debugging process is the same
3.3.2 Key functions
createSagaMiddleware
sagaMiddleware.run
rootSaga
channel(stdChannel)
The two core methods of channel are take, put. take is for inserting the taker into the queue nextTakers, put is for iterating the queue, and execute the matching taker
Taker is the next()
next
If the generator function does not end, digestEffect will be executed, and finalRunEffect will be executed in digestEffect, and finalRunEffect actually executes the runEffect function
proc
Encapsulates next and runEffect functions, and executes next
proc source code
runEffect
If it is an effect, get the corresponding runEffect from effectRunnerMap and execute it, if not, execute next() directly
For example, effect.type is 'TAKE' or 'PUT', then execute runTakeEffect or runPutEffect
effectRunnerMap
effectRunnerMap source code
effectRunnerMap defines the runners corresponding to various side effects
For example, when effect.type is 'TAKE', runTakeEffect will be executed, and if it is 'PUT', runPutEffect will be executed. Other Effects are similar
The parameter cb in runTakeEffect and runPutEffect both refers to next.
runTakeEffect is to stuff next function into the channel queue. runPutEffect is to execute store.dispatch.
3.3.3 Summarize
createSagaMiddleware(sagaMiddlewareFactory) rewrites the dispatch method, which executes channel.put. channel.put will iterate the taker (next function in proc.js) queue and execute the matching taker
proc is executed in sagaMiddleware.run(rootSaga), next is executed in proc, and runEffect is executed in next. RunEffect judges that if it is an effect, execute the effectRunner matched by effectRunnerMap, if not, execute next.
If the judgment is 'TAKE', execute runTakeEffect. The function of runTakeEffect is to put taker(next) back into the channel queue. Wait for the next UI component to call store.dispatch and trigger channel.put, iterate the channel queue, and execute the matched taker(next)
The last next is the native store.dispatch
The core function of next is to execute the generator down, which is why 'TAKE' blocks there, because it ends after putting next into the channel queue
function runTakeEffect(env, {pattern}, cb) {
...
env.channel.take(cb, matcher);
}
Stuff cb(next) into the channel queue
It will be block, because cb(next) is not called after the queue is inserted, it means the generator is not continue to executed .
4.1.2 call
function runCallEffect(env, { fn, args }, cb) {
const result = fn.apply(null, args);
if (is.promise(result)) {
return result.then(data => cb(data)).catch(error => cb(error, true));
}
cb(result);
}
If call is a promise, it will block until Promise.resolve is invoked
4.1.2 put
function runPutEffect(env, { action }, cb) {
const result = env.dispatch(action);
cb(result);
}
Dispatch action to reducer
It won't be block, because executed cb(next)
4.1.2 select
function runSelectEffect(env, { selector, args }, cb) {
const state = selector(env.getState(), ...args)
cb(state)
}
How to use
let result = yield select(state => state.xxx);
//or
let result = yield select();
If there is no function passed in select, it will return all states by executing store.getState()
function takeEvery(pattern, saga) {
function* takeEveryHelper() {
while (true) {
yield take(pattern);
yield fork(saga);
}
}
return fork(takeEveryHelper);
}
souce code
Achieve the effect of each listening by an infinite loop generator
pattern is action type
takeEvery will trigger every time an action is listened, but takeLastest will only trigger the last time
5 How to extend your own saga
5.1 Add an effect runner
5.1.1 Adding types in effectTypes
5.1.2 Add the corresponding effect runner in effectRunnerMap
6 Further analysis of the source code of key functions
export function multicastChannel() {
let closed = false
//A task queue containing all callback functions (takers)
let currentTakers = []
let nextTakers = currentTakers
...
return {
//excute every taker
put(input) {
...
const takers = (currentTakers = nextTakers)
for (let i = 0, len = takers.length; i < len; i++) {
const taker = takers[i]
if (taker[MATCH](input)) {
taker.cancel()
taker(input)
}
}
},
take(cb, matcher = matchers.wildcard) {
...
cb[MATCH] = matcher
...
//add the cb(taker)
nextTakers.push(cb)
cb.cancel = once(() => {
ensureCanMutateNextTakers()
remove(nextTakers, cb)
})
},
close,
}
}
// stdChannel function is to execute in outside
export function stdChannel() {
const chan = multicastChannel()
const { put } = chan
chan.put = input => {
if (input[SAGA_ACTION]) {
put(input)
return
}
//asap is a scheduling function that only allows one task to execute at a time
asap(() => {
put(input)
})
}
return chan
}
The take of stdChannel is equivalent to listening, and put is equivalent to publishing
Simple version
function stdChannel(){
let currentTakers = [];
function take(taker,matcher){
taker['MATCH']=matcher;
currentTakers.push(taker);
}
function put(input){
for(let i=0;i<currentTakers.length;i++){
let taker = currentTakers[i];
let matcher = taker['MATCH'];
if(matcher(input)){
taker(input);
}
}
return {take,put};
}
}
let channel = stdChannel()
6.1.6 proc
Execute the next of the Generator object iterator once, and execute the corresponding type of effect runner according to the result(effect type)
source code
export default function proc(env, iterator, parentContext, parentEffectId, meta, isRoot, cont) {
...
*
* receives either (command | effect result, false) or (any thrown thing, true)
*/
function next(arg, isErr) {
...
result = iterator.next(arg)
...
}
function runEffect(effect, effectId, currCb) {
...
} else if (effect && effect[IO]) {
const effectRunner = effectRunnerMap[effect.type]
effectRunner(env, effect.payload, currCb, executingContext)
} else {
// anything else returned as is
currCb(effect)
}
...
}
Simple version
export default function proc(env,iteraton){
function next(args){
let result;
//arg: genetor param //{done:false,value:{type:'TAKE'...}
result = iterator.next(args);
if(!result.done){
runEffect(result.value,next);
}
}
function runEffect(effect,next){
if(effect){
//different effect type to defferent effect runner
const effectRunner =effectRunnerMap[effect.type];
effectRunner(env,effect.payload,next);
}else{
next();
}
}
next();
}
//返回的middleware
return next => action => {
if (sagaMonitor && sagaMonitor.actionDispatched) {
sagaMonitor.actionDispatched(action)
}
//先执行next,hit reducer
const result = next(action) // hit reducers
//加入channel
channel.put(action)
return result
}
}
sagaMiddleware.run = (...args) => {
if (process.env.NODE_ENV !== 'production' && !boundRunSaga) {
throw new Error('Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware')
}
return boundRunSaga(...args)
}
function stdChannel(){
let currentTakers = [];
function take(taker,matcher){
taker['MATCH']=matcher;
currentTakers.push(taker);
}
function put(input){
for(let i=0;i<currentTakers.length;i++){
let taker = currentTakers[i];
let matcher = taker['MATCH'];
if(matcher(input)){
taker(input);
}
}
return {take,put};
}
}
let channel = stdChannel()
The upper part is the English version, and the lower part is the Chinese version, with the same content. If there are any wrong, or you have anything hard to understand, pls feel free to let me know.many thx.
Overview
1.The data flow
2 How to use
3 Debug the source code
3.1 How to debug the source code
3.2 Debug the execution flow of saga in chrome devtool
3.3.1 Entrance: store.js
3.3.2 Key functions
createSagaMiddleware![factory](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319172701.png)
sagaMiddleware.run
rootSaga
![rootSaga_core](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319173241.png)
channel(stdChannel)![channel](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319194337.png)
next
![fact_runEffect](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220328135713.png)
proc![proc](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319201209.png)
runEffect![runEffect](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220328144030.png)
effectRunnerMap
![putrunner](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319201910.png)
3.3.3 Summarize
4 Principle analysis of core API
4.1 Principle
4.1.1 take
4.1.2 call
4.1.2 put
4.1.2 select
How to use
4.1.2 fork
4.1.2 takeEvery & takeLastest
5 How to extend your own saga
5.1 Add an effect runner
5.1.1 Adding types in effectTypes
5.1.2 Add the corresponding effect runner in effectRunnerMap
6 Further analysis of the source code of key functions
redux-saga source code
6.1 execution flow
6.1.1 entrance in store
6.1.2 Find the source code corresponding to the entry
The entrance in the core compiled file![](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220224153916.png)
6.1.3 sagaMiddlewareFactory
source code
6.1.4 runSaga
source code
6.1.5 channel
source code
6.1.6 proc
Execute the next of the Generator object iterator once, and execute the corresponding type of effect runner according to the result(effect type) source code
Simple version
6.1.7 effectRunnerMap
source code
The following is the Chinese version, the same content as above
Overview
1. 数据流
2 如何使用
3 调试源码
3.1 如何调试源码
const sagaMiddleware = createSagaMiddleware(); const store = createStore(reducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(rootSaga);
function runTakeEffect(env, {pattern}, cb) { ... env.channel.take(cb, matcher); }
function runCallEffect(env, { fn, args }, cb) { const result = fn.apply(null, args); if (is.promise(result)) { return result.then(data => cb(data)).catch(error => cb(error, true)); } cb(result); }
function runPutEffect(env, { action }, cb) { const result = env.dispatch(action); cb(result); }
function runSelectEffect(env, { selector, args }, cb) { const state = selector(env.getState(), ...args) cb(state) }
let result = yield select(state => state.xxx);
//or let result = yield select();
function runForkEffect(env, { fn }, cb) { const taskIterator = createTaskIterator({ ...,fn}) proc(env, taskIterator); cb(); }
function takeEvery(pattern, saga) { function* takeEveryHelper() { while (true) { yield take(pattern); yield fork(saga); } }
return fork(takeEveryHelper); }
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware(); sagaMiddleware.run(rootSaga);
export default function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {}) { ... function sagaMiddleware({ getState, dispatch }) { // 为runSaga提供redux的相关参数 boundRunSaga = runSaga.bind(null, { ...options, context, channel, dispatch, getState, sagaMonitor, })
}
sagaMiddleware.run = (...args) => { if (process.env.NODE_ENV !== 'production' && !boundRunSaga) { throw new Error('Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware') } return boundRunSaga(...args) }
sagaMiddleware.setContext = props => { if (process.env.NODE_ENV !== 'production') { check(props, is.object, createSetContextWarning('sagaMiddleware', props)) }
}
return sagaMiddleware ... }
export function runSaga( //第一个参数是redux通过bind传来的,saga就是项目里定义的saga { channel = stdChannel(), dispatch, getState, context = {}, sagaMonitor, effectMiddlewares, onError = logError }, saga, ...args ) { if (process.env.NODE_ENV !== 'production') { check(saga, is.func, NON_GENERATOR_ERR) }
// 执行saga(generator函数)返回的迭代器 const iterator = saga(...args)
...
const env = { channel, dispatch: wrapSagaDispatch(dispatch), getState, sagaMonitor, onError, finalizeRunEffect, }
// immediately 是scheduler.js 任务调度方法,代表立即执行 return immediately(() => { //核心方法 proc const task = proc(env, iterator, context, effectId, getMetaInfo(saga), / isRoot / true, undefined)
}) }
export function multicastChannel() { let closed = false
}
//外界执行的 stdChannel 函数 export function stdChannel() { const chan = multicastChannel() const { put } = chan chan.put = input => { if (input[SAGA_ACTION]) { put(input) return }
}
function stdChannel(){ let currentTakers = []; function take(taker,matcher){ taker['MATCH']=matcher; currentTakers.push(taker); } function put(input){ for(let i=0;i<currentTakers.length;i++){ let taker = currentTakers[i]; let matcher = taker['MATCH']; if(matcher(input)){ taker(input); } } return {take,put}; } } let channel = stdChannel()
export default function proc(env, iterator, parentContext, parentEffectId, meta, isRoot, cont) { ... *
简化写法
6.1.7 effectRunnerMap
源码