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.