网络日志

redux-saga的基本使用及相关原理

redux-saga是什么?

众所周知, redux-saga 是一个中间件。所谓的中间件就是给redux提供额外功能的, 简而言之 , 也就是对redux中的dispatch, 加上一些功能, 进行包装。

我们为什么要使用redux-saga呢?

在redux中, 如果我们dispatch一个action之后 , 会调用reducer函数 。

 function dispatch(action) {
    state = reducer(state, action);
    listeners.forEach(l => l());
    return action;
  }

然后reducer是一个纯函数, 没有副作用 ,当我们需要进行异步请求的时候,这个时候redux就不够用了 , 因此我们需要借助于redux-saga。

redux-saga如何进行扩充redux的功能呢?

中间件的基本格式

我们在扩充中间件的时候会使用一个applyMiddleware的函数, 对原有的store.dispatch进行扩充。

function applyMiddleare(...middlewares) {
  return function (createStore) {
    return function (reducer, preloadedState) {
      let store = createStore(reducer, preloadedState);
      let dispatch;
      let middlewareAPI = {
        getState: store.getState,
        dispatch: (action) => dispatch(action),
      };
      let chain = middlewares.map((middleware) => middleware(middlewareAPI));
      dispatch = compose(...chain)(store.dispatch);
      return {
        ...store,
        dispatch,
      };
    };
  };
}
//compose函数是:
function compose(funcs){
return funcs.map((a, b) => (...args) => a(b(...args)))
}

由下面这段代码可以看出一个中间件, 是接受一个middleAPI也就是store,以及上一次改造后的dispatch, 然后返回一个新的改造后的dispatch。

 let chain = middlewares.map((middleware) => middleware(middlewareAPI));
 dispatch = compose(...chain)(store.dispatch);

所以一个中间件的格式应该是:

function middleware(store) {
  return (lastDispatch) => {
    return (action) => {
      //进行新的改造
      //比如logger中间件是加上consolelog 然后lastDispatch
      // 比如redux-thunk  是如果action是一个函数的话应该如何处理
    };
  };
}

redux-saga基本原理:

saga采用generator函数来yield effect,generator函数的作用是可以暂停执行, 下次再从上次暂停的地方继续执行。

redux-saga基本基本使用和具体实现:

v redux-saga中saga分类:
  • rootsaga,也就是saga的入口
  • 监听saga, 也就是watchsaga,一般通过yieldtake来监听
  • 具体执行的saga, 通过put fork cps等等来执行

    // 执行saga
    function * workerSaga(){
    yield put({type:actionTypes.ADD});
    }
    // 监听saga
    function * watcherSaga(){
    yield take(actionTypes.ADD_SYNC);
    yield workerSaga();
    }
    // 入口saga
    export default function* rootSaga() {
    yield watcherSaga();
    } 

我们知道在使用redux-saga的时候我们使用createSagaMiddleware来进行创建一个saga函数中间件 , 然后在saga函数添加了一个run属性,然后在调用了applyMiddleware之后执行saga.run(rootsaga)。saga.run的时候我们就把入口saga放到了redux-saga的逻辑程序里面进行执行了。现在我们想一下这个过程应该怎么去实现:

首先, createSagaMiddleware就是创建saga的中间件, 按照上面我所说的中间件的格式, 所以 createSagaMiddleware应该这样写

function createSagaMiddleware() {
  function sagaMiddleware({ getState, dispatch }) {
    return function (lastDispatch) {
      return function (action) {
        const result = lastDispatch(action);
        return result;
      }
    }
  }
  return sagaMiddleware;
}

然后需要给sagaMiddleware添加run函数, 在run函数中我们执行了一个runsaga的函数。这是一个类似于co的库, 递归调用generator函数 , 直到it.next()返回的done值是false的时候才会结束。在这期间会对不同的effect的type进行不同的处理因此实现了effect的异步执行。

sagaMiddleware.run= function (...args) {runSaga(...args)}
export default function runSaga(env, saga) {
    let { channel, dispatch } = env;
    let it = typeof saga === 'function'?saga():saga; // yield fork 的时候直接是执行了一遍runsaga()// 所以saga可能就不是一个function
    function next(value) {
        let {value:effect,done} = it.next(value);
        if (!done) {
            if(typeof effect[Symbol.iterator] === 'function'){// 这种是yield 后面接了一个saga的情况,如yield watchSaga()
                runSaga(env,effect);
                next();//不会阻止当前saga继续向后走
            }else if (effect instanceof Promise) {// 如果yield 了一个new promise 
                effect.then(next);
            }else{
                switch (effect.type) {
                    case effectTypes.TAKE:
                      // ...
                    case effectTypes.PUT:
                         // ...
                        break;
                    default:
                        break;
               }
            }

        }
    }
    next();
}

通过effect.type对不同的类型进行函数的处理。

take: 监听一次

用法:

 yield take(actiontype)

监听一次, 然后继续向下执行。

case effectTypes.TAKE:
channel.once(effect.actionType,next);

其中channel是一个类似于node EventEmitter的发布订阅模型, 我们可以实现一个。

function channel() {
  let listeners = [];
  function once(type, listener) {
    listener.type = type;
    listener.cancel = function () {
      listeners = listeners.filter((item) => item !== listener);
    };
    listeners.push(listener);
  }
  function put(action) {
    for (let i = 0; i < listeners.length; i++) {
      if (listeners[i].type === action.type) {
        //只有一次监听
        listeners[i](action);
        listeners[i].cancel();
      }
    }
  }
  return {
    once,
    put,
  };
}

那我们什么时候去派发函数让take去监听呢?没错就是我们在enhance dispatch的时候进行派发动作的,因此我们createSagaMiddleware 返回的dispatch中加入派发的动作, 进而能够让take进行监听。

function createSagaMiddleware() {
  function sagaMiddleware({ getState, dispatch }) {
    return function (lastDispatch) {
      return function (action) {
        const result = lastDispatch(action);
        channel.put(action);
        return result;
      }
    }
  }
  return sagaMiddleware;
}
fork 不会阻塞的runsaga

用法: yield fork(saga) // 返回一个task对象 , 对象里有cancel属性

case effectTypes.FORK:
   runSaga(env,effect.saga);
      next();

所以就算此时yield fork没有执行完成 , 这个时候也会继续相加执行下一个yield,因此不会堵塞, 这也是和put的区别。

takeEvery: 基于fork和take进行实现。

用法: yield takeEvery(actionType)原理是每当执行takeEvery的时候就会重新开启一个新的进程,新的进程是一个while true循环, 每个循环都会take监听


function takeEvery(type, saga){
  function *sagaHelper(){
    while(true){
     yield take(type)
      yield fork(saga)
    }

  }
  fork(sagaHelper)

}
call 可以调用promise函数

用法: yield call(promiseFn)

case effectTypes.CALL:
 effect.fn(...effect.args).then(next);
  break;
cps 回调函数的处理方式

用法:

 yield cps(fn, params1, params2)
function fn(param1, param2, callback){
callback(null,data)
}

跟promise的区别在于使用了callback的处理方式,如果出现error的话, 那么next执行的时候就it.throw()抛出异常。


case effectTypes.CPS:
      if(err){   
         effect.fn(...effect.args,(err,data)=>{                 
          }else{      
          next(err,true);
          next(data);
      }
  });
  break; 
all 都执行完成之后才继续向下执行

用法: yield all ([iterator1,iterator2])

 case effectTypes.ALL:
    const { iterators } = effect;
    let result  =[];
    let count  =  0;
    iterators.forEach((iterator, index) => {
      runSaga(env, iterator,(data)=> {
        result[index] =data;
        if(++count===iterators.length){
          next(result)
        }
      })

总结:

在redux-saga中, yield 了一个effect之后, 会在effect.js中转换成相应的effecttype,然后通过runsaga这个函数来进行不同的转换。 runsaga这个函数类似于co原理 , 递归调用next函数 , 终止条件是 it.next()返回的done是true值。