This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!

This article starts with the main process, the hooks state management library has a long way to go

preface

Previously, I rarely used Redux because of the technology stack. Because at first I thought it was very unwise to use redux indiscriminately under hooks. I’ve always thought that we should focus on the main things and not add extra complexity and mental burden to the project.

But most of the current projects are based on DVA, which… It gives me a headache.

If you are familiar with DVA, you should know that it mainly uses Redux and Redux-Saga to encapsulate the data layer (also seal the router and sever). I always told my friends that if dVA was even a framework. Isn’t every project we do a framework when it encapsulates the basic functionality?

If we don’t want to use it, then we must come up with a better alternative. So I decided to start with DVA in order to come up with a state management library that would be suitable for all aspects.

But redux doesn’t have anything on its own, so why not start with Saga

1. What is a redux-saga

Redux-saga is a library for managing application Side effects (Side effects, such as asynchronously fetching data, accessing the browser cache, etc.). The goal is to make Side effects easier to manage, more efficient to perform, easier to test, and easier to handle when a failure occurs.

Well, it’s a bit too official. Redux-saga is a redux middleware, and the most common thing we use in our work is to handle asynchronous actions. When you think about middleware that deals with asynchrony you probably know that there’s another one that has similar functionality. That’s story – thunk

1.1 story – thunk

Redux-thunk is simple, and we all know it. Redux’s so-called middleware is basically all about Dispatch. Because native Dispatches can only dispatch normal actions, we have no way to handle asynchrony. But what if you let dispatch dispatch a function?

Like this,

dispatch((dispatch,getState) = >{
    setTimeout(() = >{
        dispatch({type:'xxx'})},1000)})Copy the code

That’s ok. That’s the idea behind Redux-Thunk, so its source code is very simple.

However, Redux-Saga is different from it. Redux-saga has more powerful functions, so it is more tedious to use

1.2 The simple case of Saga

Take a look at a demo using Saga

Let’s say we implement a little example like this

1.3 Code implementation

In the component

Click the asy Add button disPatch({type:’async_add’})

The following code

import { useSelector, useDispatch } from 'react-redux'

const App = () = > {
  const number = useSelector( (state) = >state.number )
  const disPatch = useDispatch()

  const add =() = >{
    disPatch({type:'add'})}const asyncAdd=() = >{
    disPatch({type:'async_add'})}return ( 
    <div>
      { number }
      <button onClick={ add} >add</button>
      <button onClick={ asyncAdd} >asy add</button>
    </div>
   );
}
 
export default App;
Copy the code

In the store

The store is the same, but the redux-Saga has to run. You also need to pass a saga to its run method

import {
  createStore,
  applyMiddleware
} from 'redux'
import createSagaMiddleware from 'redux-saga'

import { rootSaga } from './sagas'

const reducer = (state = {
  number: 1,
}, action) = > {
  switch (action.type) {
    case 'add':
      return {
        ...state,
        number: state.number + 1
      }

      default:
        return state
  }
}


const sagaMiddleware = createSagaMiddleware()
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)


sagaMiddleware.run(rootSaga)

export default store
Copy the code

literacy

In redux-Saga, there are simply three types of saga. namely

  • Root Saga can be used as the main entry of a saga, that is, to enable all Watch saga
  • Watch Saga is responsible for monitoring dispatch behavior, and worker Saga will handle it
  • Worker Saga finally processes the corresponding logic

Let’s look at the Saga code

// Our worker Saga: Increments asynchronously
export function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'add'})}// Our watcher Saga: Spawn a new incrementAsync task in each async_add action
export function* watchIncrementAsync() {
  yield takeEvery('async_add', incrementAsync)
}

// notice how we now only export the rootSaga
// single entry point to start all Sagas at once
export  function* rootSaga() {
  yield all([
    watchIncrementAsync(),
  ])
}
Copy the code

1.4 Basic flow of work

2. Core implementation

2.1 Several core concepts

Before implementation, a few core concepts

effect api

Some of the Saga Effect apis (I prefer to call them Saga directives) are as follows. So these are all methods, and they also return an object with actionCreate

channel

The pipeline in Saga, let’s just briefly say what it does.

First saga uses a generator. Consider a scenario where we want to dispatch an action to get the interface data and then update the state in the store. Is that what we’re going to do

(Watch, worker +-+)

Use take to listen for this specific action, then fetch data, and then use put to trigger the reducer execution to update the store state

function* getData() {
  while (true) {
    yield take('async_add')
    const data = yield fetchData()
    yield put({
      type: 'updata'}}})Copy the code

Xiaohei: What’s the role of channel? Is that beside the point?

Uh, no, no. Here we go. This saga is definitely what we brought over to implement. Because the generator executes differently than a normal function, it needs to be executed to make it an iterator and then write a co to handle it.

So let’s say we have this iterator, it, and we want it to run down to a yield. So we need it.next() to begin with

You get the return value of the first yield expression, which is take(‘async_add’). Take is used to match a specific action. At this point we don’t know if the action has been fired, so the iterator cannot be executed.

At the same time, we need to call channel.take to save the function that it can continue to execute – namely, the next function (subscribe).

When the component’s action of type async_add is dispatched, we call channel.put and execute the saved function. That is to make this iterator go down.

Mm ~, don’t know so say, good not understand, (really not understand it doesn’t matter, continue to look down, for a while go through the final code ok). A channel is basically a publish subscribe. Let’s write a simple channel

/** * is just a publish-and-subscribe *@returns {take,put}* /
const channel = () = > {
  let takers = []

  /** Control the execution of co */
  const take = (taker) = > {
    taker.cancel = () = > {
      takers = takers.filter(item= >item ! == taker); } takers.push(taker) }/** When the page has action come */
  const put = (action) = > {
    takers.forEach(taker= > {
      taker.cancel();
      taker(action)
    })
  }

  return {
    take,
    put
  }
}

export default channel
Copy the code

3. The source code

createSagaMiddleware

Redux’s middleware format is basically dead

The only difference here is that sagaMiddleware has a run method, and for the run method to get getState and Dispatch from the store. We need to do something inside the sagaMiddleware function. Because this is the only place to get what we want.

We can also call channel.put(action) here because the next function is the dispatch used by our component.

As soon as the goods arrive, notify the subscriber to deal with the corresponding logic. That is, make the iterator go down quickly

import createChannel from './createChannel';
import runSaga from './runSaga';

const createSagaMiddleware=() = >{
  /** Create pipe */
  let channel = createChannel();
  let boundRunSaga;

  /**sagaMiddleware */
  const sagaMiddleware=({getState,dispatch}) = >{
    /** The purpose is to get getState, dispatch, etc */ in runSaga
    boundRunSaga = runSaga.bind(null,{getState,dispatch,channel})
    return (next) = >{
        return (action) = >{ next(action) channel.put(action); }}}/** Run method of sagaMiddleware */
  sagaMiddleware.run=(saga) = >boundRunSaga(saga)
  return sagaMiddleware
}

export default createSagaMiddleware
Copy the code

runSaga

This runSaga is the final run method

So what’s the main thing that’s going on here? What I’ve written here is very simple. You can ignore immediately, I copied it from the source code…

You can understand that functions passed to immediately are called immediately and return

Just like that return immediately (cb) ===return cb()

The main idea here is to hand over the iterator to a CO, which is obviously what the proc does here

import {
  immediately
} from './scheduler'
import proc from './proc'

/**
 * run saga
 * @param {*} param0 
 * @param {*} saga 
 */

const runSaga = (env, saga) = > {

  let it = typeof saga === 'function' ? saga() : saga

  return immediately(() = > {
    const task = proc(env, it)
    return task
  })

}

export default runSaga
Copy the code

proc

The logic of CO mainly uses recursion. Here, the corresponding processing is mainly done through the type of effect passed by the yiled expression

const proc = (env, it) = > {

  const {
    getState,
    dispatch,
    channel
  } = env

  const next = (value, isError) = > {
    let result
    result = it.next(value)
    let {
      value: effect,
      done
    } = result

    if(! done) {switch (effect.type) {
        case 'take':
          channel.take(next)
          break;
        case 'put':
          dispatch(effect.actionType);
          next()
          break;
        default:
          if(typeof effect.then==='function'){
            effect.then(res= >{
              if(res){
                next(res)
              }else{
                next()
              }
            })
          }
          break; }}else{
      return
    }
  }

  next()
}

export default proc
Copy the code

effect

This one is a little bit easier, and it says it’s pretty much the same as actionCreate.

export const take = (actionType) = > {
  return {
    type: 'take',
    actionType
  };
}

export const put = (actionType) = > {
  return {
    type: 'put',
    actionType
  };
}
Copy the code

4. Try it out

Come on, the basics are all written down. Now it’s time for mules and horses to take a walk. Replace everything about Saga with your own

An interface requests demo

Let’s see what happens first

My mock data

Our saga

import {
  take,
  put
} from '.. /redux-saga/effect'

const fetchData = () = > fetch('http://localhost:5001/demo').then(res= > res.json())

function* demo() {
  while (true) {
    yield take('async_add')
    const data = yield fetchData()
    yield put({
      type: 'updata'.payload:data.data
    })
  }
}

export default demo
Copy the code

Change some store things

import {
  createStore,
  applyMiddleware
} from 'redux'
import createSagaMiddleware from '.. /redux-saga'

// import { rootSaga } from './sagas'
import demo from './my_saga'

const reducer = (state = {
  number: 1,
  data: ' '
}, action) = > {
  switch (action.type) {
    case 'add':
      return {
        ...state,
        number: state.number + 1
      }
      case 'updata':
        return {
          ...state,
          data: action.payload
        }
        default:
          return state
  }
}


const sagaMiddleware = createSagaMiddleware()
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)


sagaMiddleware.run(demo)

export default store
Copy the code

Component code tuning

import { useSelector, useDispatch } from 'react-redux'

const App = () = > {
  const state = useSelector( (state) = >{return {number:state.number,data:state.data}} )
  const disPatch = useDispatch()

  const add =() = >{
    disPatch({type:'add'})}const asyncAdd=() = >{
    disPatch({type:'async_add'})}return ( 
    <div>
      { state.number }
      { state.data }
      <button onClick={ add} >add</button>
      <button onClick={ asyncAdd} >asy add</button>
    </div>
   );
}
 
export default App;
Copy the code

5. Write to the end

My code here is somewhat different from the source code, mainly for the convenience of implementation. The emphasis is still on learning ideas.

Now only run through some simple API, there is a need to take to github: github.com/gong9/saga-…

Reference:

zhuanlan.zhihu.com/p/30098155

Github.com/redux-saga/…

redux-saga-in-chinese.js.org/