【周末充电~】来探究一下redux-saga的实现

797 阅读8分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

本篇先来跑个主要流程,对于hooks状态管理库的探究还要持续很长一段时间

前言

以前,由于技术栈的原因我很少使用redux。因为开始我认为hooks下不分青红皂白的盲目使用redux显然是非常不明智了。我一直认为我们应该把主要精力放到主要的事情上,不应该给项目引入的额外的复杂性额外的心智负担。

但是最近手头上的项目基本全都是基于dva的,这...就让我很头疼。

我其实是有点歧视dva的,熟悉dva的朋友应该都知道,它主要就是借助redux和redux-saga做了一下数据层的封装(也封了下router和sever)。我开始总是跟朋友说要是dva都能算得上是一个框架的话。那我们做的所有项目其刚封装好基础功能时岂不是都可以算的上一个框架?

但是抱怨归抱怨,如果想不用它,那么咱就必须拿出一个更好的替代品。所以为了想整一个哪方面都很合适的状态管理库,我决定先从dva下手研究一下。

但是呢redux本身没有什么东西,所以那就从saga开始吧~

1. 什么是redux-saga呢

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

额,有点太官方了,redux-saga就是一个redux的中间件,在工作中最常见的就是我们使用它来处理异步action。提起处理异步的中间件相比大家应该都知道还有一个也是和它有着类似的功能。那就是redux-thunk

1.1 redux-thunk

redux-thunk这个东西很简单,我们都清楚。redux所谓的中间件基本上都是围绕着dispatch做文章。因为原生的dispatch只能派发普通的action,所以我们没有办法处理异步。但是如果让dispatch可以派发一个函数呢?

像这样

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

那不就ok了嘛,这就是redux-thunk的思路,所以它的源码也非常简单。

但是redux-saga和它不同,redux-saga功能更加强大,故使用上也更为繁琐一些

1.2 saga的简单案例

来看一下使用saga的一个demo

比如我们实现这么一个小例子

01.gif

1.3 代码实现

组件中

点击asy add按钮 disPatch({type:'async_add'})

代码如下

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;

store中

store中也是老三样,不同的是redux-saga要跑起来。还需要传一个saga到它的run方法里

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

扫盲

在redux-saga中,可以简单把saga划分为三种。即

image-20210718163837886.png

  • root saga可当作一个saga的主入口,即启用所有watch saga
  • watch saga负责监听dispatch过来的行为,然后交由worker saga去处理
  • worker saga 最终处理相应逻辑

即来看saga代码

// Our worker Saga: 将执行异步的 increment 任务
export function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'add' })
}

// Our watcher Saga: 在每个 async_add action spawn 一个新的 incrementAsync 任务
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(),
  ])
}

1.4 工作的基本流程

基本流程.png

2. 核心实现

2.1 几个核心概念

实现之前,先来说几个核心概念

effect api

saga的一些effect api(我更喜欢把他们叫做saga的指令)主要有下面这些。这些都是一些方法,他们和actionCreate一下,也是返回一个对象

image-20210718164639434.png

channel

saga中的管道,先简单说一下它主要做的事情。

首先saga借助了generator。如一个场景我们想要要派发一个action来获取接口数据然后更新store中的state。我们是不是会这样处理

(watch、 worker 写一块了 +-+ )

用take监听这个特定的action,然后去拿数据,再然后用put去触发reducer的执行从而更新store的state

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

小黑:channel的作用勒?是不是跑题了?

呃,没有没有。这不就来了。这个saga我们拿过来肯定是要执行它。因为生成器的执行和普通函数不一样,故需要执行一下将它变为迭代器,然后写个co去处理。

比如说这时候我们已经拿到这个迭代器it了,我们肯定需要它先往下执行跑到一个yield的地方。故开始我们就需要it.next()

这时拿到了第一个yield表达式的返回值,是个take('async_add')。take的作用是在匹配某个特定的action。这个时机我们不知道组件中有没有这个action被触发,所以这个迭代器就不能往下执行了。

同时我们需要调用channel.take将可以让它继续往下执行的函数存下来---也就是next函数 (订阅)

等到组件中type为async_add的这个action被派发时 我们调用channel.put,执行上面存下来的那个函数。也就是让这个迭代器可以继续向下执行(发布)

嗯嗯~,不知道这么说,好不好明白,(实在不好明白也没关系,继续向下看,一会走一遍最后的代码就ok了)。说白了channel主要就是一个发布订阅。来写一个简单的channel

/**
 * 其实就是一个发布订阅
 * @returns {take,put}
 */
const channel = () => {
  let takers = []

  /**控制co的执行 */
  const take = (taker) => {
    taker.cancel = () => {
      takers = takers.filter(item => item !== taker);
    }
    takers.push(taker)

  }

  /**当页面有action过来 */
  const put = (action) => {
    takers.forEach(taker => {
      taker.cancel();
      taker(action)
    })
  }

  return {
    take,
    put
  }
}

export default channel

3. 源码

createSagaMiddleware

redux的中间件的格式基本已经是死的了

这里唯一的不同就是sagaMiddleware多了一个run方法,同时为了run方法可以拿到store上的getStatedispatch。我们需要在sagaMiddleware函数里面做点手脚。因为只有这里可拿到我们想要的东西。

同时在这里next函数就是我们组件派发使用的dispatch,故我们可以在这里调用 channel.put(action)

即来货了,赶紧通知订阅者处理相应逻辑。也即赶紧使迭代器往下走

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

const createSagaMiddleware=()=>{
  /**创建管道 */
  let channel = createChannel();
  let boundRunSaga;

  /**sagaMiddleware */
  const sagaMiddleware=({getState,dispatch})=>{
    /**目的是想在runSaga中拿到getState、dispatch等*/
    boundRunSaga = runSaga.bind(null,{getState,dispatch,channel})
    return (next)=>{
        return (action)=>{
          next(action)
          channel.put(action);
        }
    }
  }
  /**sagaMiddleware的run方法 */
  sagaMiddleware.run=(saga)=>boundRunSaga(saga)
  return sagaMiddleware
}

export default createSagaMiddleware

runSaga

这个runSaga就是最终的run方法了

这里主要做了什么呢?我这里写的很简单。可以忽略immediately哈,这是我从源码中copy来的...

你可以理解为传给immediately的函数立即被调用并且return回来

就像这样return immediately(cb)===return cb()

这里主要就是想迭代器交给一个co去处理,很明显proc在这里就是干这个活的

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

proc

co的逻辑主要就是利用递归了,这里主要就是通过yiled表达式传过来的effect的类型做一下相应的处理

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

effect

这个就比较简单了,上面也说了它和actionCreate差不多。

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

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

4. 试下效果

来吧基本的东西都写好了,现在是骡子是马可以出来溜溜了。把关于saga的东西都换成自己的

一个接口请求demo

直接先看效果

02.gif

我的mock数据

image-20210718182031735.png

我们的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

更改一些store的东西

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

组件代码调整

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;

5. 写到最后

我这里的代码有一些地方和源码不太一样,主要是为了方便实现。其重点还是学习思想。

现在仅跑通了一些简单api,有需要的可以拿去 github:github.com/gong9/saga-…

参考:

zhuanlan.zhihu.com/p/30098155

github.com/redux-saga/…

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