redux-saga 实现原理

redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。

作为 redux 的中间件,redux-saga 提供了更加优雅的处理异步action的方式,redux-saga通过Generator函数来决定每次动作的暂停执行延迟取消等操作,
一个 saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。

在redux中使用redux-saga

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

import reducer from './reducers'
import mySaga from './sagas'

// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(reducer,applyMiddleware(sagaMiddleware))

// then run the saga
sagaMiddleware.run(mySaga)

// render the application

Middleware API

createSagaMiddleware

createSagaMiddleware函数目的是创建sagaMiddleware中间件,同时我们也知道sagaMiddleware函数绑定了run方法

function createSagaMiddleware(){
  return function sagaMiddleware({dispatch,getState}){
    // 给sagaMiddleware绑定run函数
    sagaMiddleware.run = function(generator){}
    // 中间件函数
    return (next) => (action) => {
      next(action);
    }
  }
}

middleware.run

sagaMiddleware.run = function (generator, callback) {
  // 判断 generator 是否函数,返回执行结果或者本身
  const iterator = typeof generator === "function" ? generator() : generator;
  // next 函数判断下一次的动作
  function next(action){
    const { value: effect, done } = iterator.next();
    //如果generator完成,则执行回调函数
    if(done){
      callback && callback();
      return;
    }
    // 如果 effect 是一个 generator
    if (typeof effect.next === "function"){
      run(effect,next);
    }
    // 如果 effect 是一个 Promise,在promise结束后继续执行next
    if (effect instanceof Promise){
       effect.then(next)
    }
  }
  next();
}

Effect 创建器

redux-saga可以看成由一个个的 effect 所组成,effect 代表了每次执行的指令,新建 effects.js 文件用来创建 effect

take

take函数能够阻塞generator,只有在中间件action中触发才能往下执行,这里可以理解成发布订阅的一个操作,可以在内部创建一个channel函数用来创建发布订阅器:

function createChannel() {
  let listener = {};
  let subscribe = (actionType, callback) => listener[actionType] = callback;
  let publish = (action) => {
    if (!listener[action.type]) return;
    let actionFn = listener[action.type];
    delete listener[action.type];
    actionFn(action);
  }
  return { subscribe, publish };
}
const channel = createChannel();
...
return (next) => (action) => {
  channel.publish(action); //中间件派发每次action动作
  next(action);
}
...

effects文件中创建take函数,导出并声明type类型:

// effects.js
export function take(actionType){
  return {
    type:"TAKE",
    actionType
  }
}

next函数中,判断effect类型,然后订阅actionType,并将下一次的next操作传递给订阅函数,当中间件函数中捕获到action时,才能触发到下一次的函数执行:

switch (effect.type) {
  case "TAKE":
    channel.subscribe(effect.actionType,next);
    break; 
}

put

put方法执行action操作,直接在effect函数中返回action,next函数中截取到并dispatch

// effects.js
export function put(action){
  return {
    type:"PUT",
    action
  }
}
case "PUT":
  dispatch(effect.action);
  next(effect.action);
  break;

fork

effects函数中创建fork

// effects.js
export function fork(task) {
  return {
    type:"FORK",
    task
  }
}

next中截取到FORK类型,task可能是个generator函数,所以传递给run方法执行,
并且保存forkTask,方便后面做取消任务的操作:

case "FORK":
  let forkTask = effect.task();
  sagaMiddleware.run(forkTask);
  next(forkTask);
  break;

cancel

generator中可以通过return方法终止任务的执行,所以我们可以拿到前面fork保存的generator,进行终止操作:

// effects.js
export function cancel(task) {
  return {
    type:"CANCEL",
    task
  }
}
case "CANCEL":
  effect.task.return('over')
  break;

takeEvery

takeEvery可以理解成对fork的封装,用while将每一次的任务监听起来:

//effects.js
export function* takeEvery(actionType,task) {
  yield fork(function* (){
    while(true){
      yield take(actionType);
      yield task();
    }
  })
}

call

call方法接受promise函数并执行:

//effects.js
export function call(fn,...args) {
  return {
    type:"CALL",
    fn,
    args
  }
}

所以在next中,如果是call类型则在promise结束后才执行下一个next

case "CALL":
  effect.fn(...effect.args).then(next)
  break;

cps

call方法接受回调函数并执行:

//effects.js
export function cps(fn,...args) {
  return {
    type:"CPS",
    fn,
    args
  }
}
case "CPS":
  effect.fn(...effect.args,next);
  break;

all

all方法需要等所有的generator函数执行完才能往下执行

//effects.js
export function all(fns) {
  return {
    type:"ALL",
    fns
  }
}
/*
* @params cb [function] 回调函数
* @params total [number] 执行数量
*/
function times(cb, total) {
  let index = 0;
  return () => {
    index++;
    if (index >= total) {
      cb && cb();
    }
  }
}

...
case "ALL":
  const total = effect.fns.length;
  const over = times(next, total);
  effect.fns.forEach(fn => run(fn, over))
  break;
...