The original link

If you are interested or interested in US stocks, you can add me on wechat: Xiaobei060537, and communicate with me at 😝.

Redux-saga is a middleware that manages the asynchronous operations of Redux applications, similar to redux-Thunk + async/await. It stores all the asynchronous operations logic in one place for central processing by creating Sagas.

Story – effects of saga

Effects in Redux-Saga is a plain text JavaScript object that contains some instructions that will be executed by Saga Middleware. These instructions perform three kinds of operations:

  • Make an asynchronous call (such as an Ajax request)
  • Initiate other actions to update the Store
  • Call other Sagas

There are a number of instructions included in Effects that you can refer to in the asynchronous API reference

The characteristics of the story – saga

  • Easy to test, for example:
assert.deepEqual(iterator.next().value, call(Api.fetch, '/products'))
Copy the code
  • Actions can be kept pure, and asynchronous operations are handled centrally in a saga
  • Watch /worker (listen -> execute
  • Is implemented as a generator
  • Supports application scenarios with complex asynchronous logic
  • Implementing asynchronous logic at a more granular level makes the process clearer and bugs easier to track and resolve.
  • Writing asynchronous logic in a synchronous way is more in line with human thinking logic

From the story – thunk to story – saga

Suppose you have a scenario where a user needs to verify that the user’s username and password are correct when logging in.

Use redux-Thunk

Get user data logic (user.js):

// 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 }); }}Copy the code

Verify the logon logic (login.js):

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 }); }}Copy the code

redux-saga

Asynchronous logic can be written entirely to saga.js:

export function* loginSaga() {
  while(true) {
    const { user, pass } = yield take(LOGIN_REQUEST) // Wait for the action LOGIN_REQUEST specified on the Store
    try {
      let { data } = yield call(loginRequest, { user, pass }); // Block to request background data
      yield fork(loadUserData, data.uid); // Non-blocking loadUserData
      yield put({ type: LOGIN_SUCCESS, data }); // Initiates an action, similar to 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 }); }}Copy the code

The difficulties in reading

For Redux-Saga, there are still many difficult to understand and obscure places. The author will sort out the concepts that are easily confused in the following:

Take the use of

Take and takeEvery both listen for an action, but they have different effects. TakeEvery responds every time an action is triggered, while take only responds when the execution flow executes the take statement. TakeEvery just listens for the action and executes the corresponding handler. It doesn’t have much control over when to execute the action and how to respond to it. The called tasks don’t have control over when to call it, and they don’t have control over when to stop listening. It can only be called over and over again each time the action is matched. But take can be used in the generator function to determine when to respond to an action and what to do after the response. For example, when listening for all types of actions to trigger a logger, use takeEvery as follows:

import { takeEvery } from 'redux-saga'

function* watchAndLog(getState) {
  yield* takeEvery('*', function* logger(action) {
      // Do some logger operation // Inside the callback function})}Copy the code

Use take to implement the following:

import { take } from 'redux-saga/effects'

function* watchAndLog(getState) {
  while(true) {
    const action = yield take('*')
    // Do some logger operation // Parallel to take})}Copy the code

While (true) means to start a new iteration (Logger) by waiting for a new arbitrary action once the last step of the process (Logger) has been reached.

Blocking and non-blocking

A call operation is used to initiate an asynchronous operation. For a generator, a call is a blocking operation that cannot perform or process anything else until the generator call has ended. However, fork is a non-blocking operation. When fork transfers a task, the task is executed in the background, and the flow of execution can continue without waiting for the result to return.

For example, the following login scenario:

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'))
    }
  }
}
Copy the code

If the result is not returned when the call requests authorize, but the user triggers the LOGOUT action at this time, the LOGOUT at this time will be ignored and not processed, because loginFlow is blocked in Authorize. Did not execute at take(‘LOGOUT’)

Perform multiple tasks at once

If you encounter a scenario where you need to perform multiple tasks at the same time, such as requesting users data and Products data, you should use the following method:

import { call } from 'redux-saga/effects'
// Execute synchronously
const [users, products] = yield [
  call(fetch, '/users'),
  call(fetch, '/products')
]

/ / instead
// Execute sequentially
const users = yield call(fetch, '/users'),
      products = yield call(fetch, '/products')
Copy the code

When the yield is followed by an array, the operations in the array will be executed according to promise. all, and genertor will block until all effects have been executed

The source code interpretation

In every project that uses Redux-Saga, the main file contains the following logic for adding sagas middleware to the Store:

const sagaMiddleware = createSagaMiddleware({sagaMonitor})
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)
Copy the code

CreateSagaMiddleware is redux-Saga core source file SRC /middleware.js.

export defaultfunction 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
    }
  }
 ...
 
 }
Copy the code

This logic mainly executes sagaMiddleware(), which assigns runSaga to sagamiddleware.run and executes it, then returns middleware. Moving on to the logic of 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
}
Copy the code

This function returns a task that was generated by the proc. Step 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)
	  } 
  ...
  }
}
Copy the code

DigestEffect executes effectTriggerd() and runEffect(), which is the effect. RunEffect () defines the functions that execute the different effects. Each effect function is implemented in proc.js.

In addition to the core methods, Redux-Saga provides a series of helper files that return an iterator-like object for subsequent traversal and execution, which will not be discussed here.

Reference documentation

  • Redux-saga Chinese document
  • From Redux-Thunk to Redux-Saga practices