redux异步action_redux-saga实现原理及umi, dva设计思想解析

e683064e-b233-eb11-8da9-e4434bdf6706.png

redux-saga中文文档:https://redux-saga-in-chinese.js.org/docs/introduction/index.html

关于redux-saga一直想总结一篇全面解析的文档,网上有很多帖子讲redux-saga,不过对于初学者来说理解起来有点抽象,对于框架dva,umi也是简单的使用。希望读者在看过本文之后,能够知道dva怎样集成redux-saga,redux-saga怎样实现异步和action监听,以及umi又是如何设计的。之前总结过redux设计思想和源码分析的文档,放到了csdn上,分别从源码,函数式编程,中间件,设计模式等方面讲解了redux。本文将来说说redux一个重要的中间件redux-saga。

之所以说redux-saga重要,不是因为其用途,redux-saga的功能也可以通过其他方式实现,但是redux-saga在内部使用了generator作为核心语法,并且有两个开源框架dva,umi与之相联系,这就决定了redux-saga的地位。

近期一直研究node IOC框架,总算抽出时间,补上这篇文档,本文将从redux-saga实现原理,redux-saga的设计思想,redux-saga和dva的关系,umi和dva的关系,dva和umi框架现状五个方面切入,之后引出对前端设计模式的思考。欢迎各位大佬指正不足。

一,redux-saga实现原理

1,用法:看一个框架和库的原理,首先要找到其入口,首先redux-saga的用法:

import React from 'react';
import App from './app.js';
import { createStore, applyMiddleware, compose } from 'redux'
import reducer from './redux/reducer'
import  createSagaMiddleware  from 'redux-saga'
import rootSaga from './redux/saga/saga'
import { Provider } from 'react-redux'

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
        reducer,
        compose(
          applyMiddleware(sagaMiddleware),
          window.devToolsExtension ? window.devToolsExtension() : compose
        )
      )
sagaMiddleware.run(rootSaga);

redux-saga和其他redux中间件的使用是有区别的,除了正常的引入中间件还需要两个步骤:

  • const sagaMiddleware =createSagaMiddleware();
  • 在通过applyMiddleware引入后,执行sagaMiddleware.run(rootSaga);

这也就找到了入口

runSaga接收的参数,一般用法:

import { fork } from 'redux-saga/effects'
...
export default function* rootSaga() {
  yield fork(setStepStatus)
}

接收了一个generator函数,里面的异步是fork或者其他effects

2,分析sagaMiddlewaresagaMiddleware.run

首先对effects进行分析,常用的effects有call, put, take, takeEvery, fork等,部分源码:

export {
  take,
  takeMaybe,
  put,
  putResolve,
  all,
  race,
  call,
  apply,
  cps,
  fork,
  spawn,
  join,
  cancel,
  select,
  actionChannel,
  cancelled,
  flush,
  getContext,
  setContext,
  delay,
} from './internal/io'

export { debounce, retry, takeEvery, takeLatest, takeLeading, throttle } 
from './internal/io-helpers'

挑选一个effect进行分析,比如take源码(验证已经去掉):

export function take(patternOrChannel = '*', multicastPattern) {
  if (is.pattern(patternOrChannel)) {
    return makeEffect(effectTypes.TAKE, { pattern: patternOrChannel })
  }
  if (is.multicast(patternOrChannel) && is.notUndef(multicastPattern) && is.pattern(multicastPattern)) {
    return makeEffect(effectTypes.TAKE, { channel: patternOrChannel, pattern: multicastPattern })
  }
  if (is.channel(patternOrChannel)) {
    return makeEffect(effectTypes.TAKE, { channel: patternOrChannel })
  }
}

可以看到take根据patternOrChannel共有三种情况,最终调用了makeEffect()方法,该方法接收两个参数。对于take的三种情况先不讨论,先看下makeEffect.

源码:

const makeEffect = (type, payload) => ({
  [IO]: true, //这里的IO是一个常量,值为@@redux-saga/IO
  combinator: false, //是否进行组合
  type,
  payload,
})

第一个参数是一个常量,代表当前effect的类型,这里都是'TAKE'

payload是传入的pattern,channel组合而成的对象。

于是可以得出,take返回的是一个带有type的对象:

{
  '@@redux-saga/IO': true, //这里的IO是一个常量,值为@@redux-saga/IO
  combinator: false, //是否进行组合
  type:"TAKE"
  payload:{ 
    channel: patternOrChannel, 
    pattern: multicastPattern 
  }
}

其余的effect返回的对象与take类似,只是payload的内容会有差别

返回的对象有了,那么payload到底是什么呢,继续=>

对于各个effect接收的参数做下分类:

  • 第一类接收函数的effect:

call, cps, fork,spawn,apply

最终payload调用:getFnCallDescriptor(fnDescriptor, args)

源码:

function getFnCallDescriptor(fnDescriptor, args) {
  let context = null
  let fn

  if (is.func(fnDescriptor)) {
    fn = fnDescriptor
  } else {
    if (is.array(fnDescriptor)) {
      ;[context, fn] = fnDescriptor
    } else {
      ;({ context, fn } = fnDescriptor)
    }

    if (context && is.string(fn) && is.func(context[fn])) {
      fn = context[fn]
    }
  }

  return { context, fn, args }
}
  • 第二类接收effect的effect

all, race,这里effect是对其他effects的组合

  • 第三类接收action, channel, pattern等的effect

put, take等(这里主要介绍这两种)

官方对于take的介绍是告诉middleware监听一个特定的action,那么这种监听到底是如何实现的呢。这也是redux-saga关于流程控制的核心设计思想。

使用实例:

while (true) {
    let request = yield take(actionTypes.GET_MATERIALS_RESOURCE);
    ...
}

take接收的是一个action类型,也就是我们在redux的action中定义的type,也可以是,如果是*,代表匹配所有的actionType

至于为什么要放在while(true)里面,下面会介绍到。

take接收到的参数有三种类型,这里以最简单的类型介绍,也就是接收的是字符串:

比如传入:"get_data",此时take返回的就是我们经常写的action对象:

{
  '@@redux-saga/IO': true, //这里的IO是一个常量,值为@@redux-saga/IO
  combinator: false, //是否进行组合
  type:"TAKE"
  payload:{ 
    pattern: "get_data"
  }
}

实例:假设现在有一个saga对象,返回内容如下:

export default function* setLoadUrl() {
  while (true) {
    let request = yield take(actionTypes.GET_MATERIALS_RESOURCE);
  }
}

接下来rootSaga引入setLoadUrl.js:

import { fork } from 'redux-saga/effects'
import setLoadUrl from './setLoadUrl'

export default function* rootSaga() {
  yield fork(setLoadUrl)
}

从上面的分析可以知道,fork接收的是函数,最终生成的函数对象是:

{
  '@@redux-saga/IO': true, //这里的IO是一个常量,值为@@redux-saga/IO
  combinator: false, //是否进行组合
  type:"FORK"
  payload:{ 
    context:null,//这里没有传入context,如果传入fork,格式是[context,fn]
    fn:setLoadUrl,
    args:undefined//这里没有传入参数
  }
}

接下来就要回到redux-saga的入口了,(上面的过程主要分析了rootSaga到底是什么)

  • const sagaMiddleware =createSagaMiddleware();
  • 在通过applyMiddleware引入后,执行sagaMiddleware.run(rootSaga);

部分源码:

export default function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {}) {
  let boundRunSaga
  function sagaMiddleware({ getState, dispatch }) {
    boundRunSaga = runSaga.bind(null, {
      ...options,
      context,
      channel,
      dispatch,
      getState,
      sagaMonitor,
    })

    return next => action => {
      if (sagaMonitor && sagaMonitor.actionDispatched) {
        sagaMonitor.actionDispatched(action)
      }
      const result = next(action) // hit reducers
      channel.put(action)
      return result
    }
  }

  sagaMiddleware.run = (...args) => {
    return boundRunSaga(...args)
  }
...
}

createSagaMiddleware返回的sagaMiddleware函数,一个基本的redux中间件结构。

agaMiddleware.run(rootSaga)指向了boundRunSaga.

boundRunSaga是runSaga指定this指向后的函数,看下runSaga

部分源码:

export function runSaga(
  { channel = stdChannel(), dispatch, getState, context = {}, sagaMonitor, effectMiddlewares, onError = logError },
  saga,
  ...args
) {
  
  const iterator = saga(...args)

  const effectId = nextSagaId()

  if (sagaMonitor) {
    // monitors are expected to have a certain interface, let's fill-in any missing ones
    sagaMonitor.rootSagaStarted = sagaMonitor.rootSagaStarted || noop
    sagaMonitor.effectTriggered = sagaMonitor.effectTriggered || noop
    sagaMonitor.effectResolved = sagaMonitor.effectResolved || noop
    sagaMonitor.effectRejected = sagaMonitor.effectRejected || noop
    sagaMonitor.effectCancelled = sagaMonitor.effectCancelled || noop
    sagaMonitor.actionDispatched = sagaMonitor.actionDispatched || noop

    sagaMonitor.rootSagaStarted({ effectId, saga, args })
  }

  if (process.env.NODE_ENV !== 'production') {
    if (is.notUndef(dispatch)) {
      check(dispatch, is.func, 'dispatch must be a function')
    }

    if (is.notUndef(getState)) {
      check(getState, is.func, 'getState must be a function')
    }

    if (is.notUndef(effectMiddlewares)) {
      const MIDDLEWARE_TYPE_ERROR = 'effectMiddlewares must be an array of functions'
      check(effectMiddlewares, is.array, MIDDLEWARE_TYPE_ERROR)
      effectMiddlewares.forEach(effectMiddleware => check(effectMiddleware, is.func, MIDDLEWARE_TYPE_ERROR))
    }

    check(onError, is.func, 'onError passed to the redux-saga is not a function!')
  }

  let finalizeRunEffect
  if (effectMiddlewares) {
    const middleware = compose(...effectMiddlewares)
    finalizeRunEffect = runEffect => {
      return (effect, effectId, currCb) => {
        const plainRunEffect = eff => runEffect(eff, effectId, currCb)
        return middleware(plainRunEffect)(effect)
      }
    }
  } else {
    finalizeRunEffect = identity
  }

  const env = {
    channel,
    dispatch: wrapSagaDispatch(dispatch),
    getState,
    sagaMonitor,
    onError,
    finalizeRunEffect,
  }

  return immediately(() => {
    const task = proc(env, iterator, context, effectId, getMetaInfo(saga), /* isRoot */ true, undefined)

    if (sagaMonitor) {
      sagaMonitor.effectResolved(effectId, task)
    }

    return task
  })
}

乍一看一头雾水,这是什么,许多变量的含义还不清楚,先不care,继续分析。

runSaga最终返回的是immediately(callback),来看看这个是什么,对应源码:scheduler

const queue = []
let semaphore = 0 //英 [ˈseməfɔː(r)] 这里做信号讲
function exec(task) {
  try {
    suspend()
    task()
  } finally {
    release()
  }
}

export function asap(task) { //尽快执行任务
  queue.push(task)
  if (!semaphore) {
    suspend()
    flush()
  }
}

export function immediately(task) { //立即执行任务
  try {
    suspend()
    return task()
  } finally {
    flush()
  }
}

function suspend() { semaphore++ } //暂停信号,也就是暂停

function release() { semaphore-- } //释放信号,也就是开始
function flush() { //执行queue中的所有tasks
  release()
  let task
  while (!semaphore && (task = queue.shift()) !== undefined) {
    exec(task)
  }
}

有没有被惊到,scheduler源码如此简洁,里面的方法也不复杂,那么到底是做什么的呢。

暴露出两个方法,asap(task), immediately(task),分别是尽快执行,立即执行。

scheduler维护了queue队列,用来存储任务,非立即执行时会把任务方法queue中,立即执行时会暂停正在执行的任务,执行完当前任务后继续执行之前的任务队列,执行每一个任务时,会把信号置为暂停,执行完之后,释放信号,执行下一个任务。

scheduler充当了任务调度的角色。

runSaga产生了一个什么样的任务呢,继续分析:

() => {
  const task = proc(env, iterator, context, effectId, getMetaInfo(saga),true, undefined)
  return task
}

考虑最简单的情况(未传入sagaMonitor)

task调用了proc, getMetaInfo

getMetaInfo(saga)的saga就是我们传入的rootSaga,getMetaInfo是获取meta;

proc的第二个参数iterator就是通过rootSaga生成的遍历器对象,自带有方法next(),

effectId是一个自增的数值,初始值为0,每次执行

源码:

function runSaga(
  { channel = stdChannel(), dispatch, getState, context = {}, sagaMonitor, effectMiddlewares, onError = logError },
  saga,
  ...args
) {
  if (process.env.NODE_ENV !== 'production') {
    check(saga, is.func, NON_GENERATOR_ERR)
  }
const iterator = saga(...args)//saga就是rootSaga

proc内部有next()方法,类似于执行generator的next方法,返回task对象:

const task = newTask(env, mainTask, parentContext, parentEffectId, meta, isRoot, cont)

const executingContext = {
  task,
  digestEffect,
}
next()

  // then return the task descriptor to the caller
return task

proc最终会调用effectRunnerMap类,根据effect的类型,执行对应的effectRunner

流程图:

最终runSaga和中间件里面的channel.put()通过channel联系了起来,也就是说用户发起dispatch后,通过channel触发runSaga(rootSaga)放入channel的effect。

那么channel是什么样的数据结构呢。

现在看下channel的数据结构:

channel文件包含了两个基本结构:channel和multicastChannel,上面由于没有传入channe,channel会调用默认设置stdChannel,stdChannel调用multicastChannel。

multicastChannel源码:

function multicastChannel() {
  let closed = false
  let currentTakers = []
  let nextTakers = currentTakers

  function checkForbiddenStates() {

  const ensureCanMutateNextTakers = () => {
    if (nextTakers !== currentTakers) {
      return
    }
    nextTakers = currentTakers.slice()
  }

  const close = () => {
    closed = true
    const takers = (currentTakers = nextTakers)
    nextTakers = []
    takers.forEach(taker => {
      taker(END)
    })
  }

  return {
    [MULTICAST]: true,
    put(input) {
      if (closed) {
        return
      }

      if (isEnd(input)) {
        close()
        return
      }

      const takers = (currentTakers = nextTakers)

      for (let i = 0, len = takers.length; i < len; i++) {
        const taker = takers[i]

        if (taker[MATCH](input)) {
          taker.cancel()
          taker(input)
        }
      }
    },
    take(cb, matcher = matchers.wildcard) {
      if (closed) {
        cb(END)
        return
      }
      cb[MATCH] = matcher
      ensureCanMutateNextTakers()
      nextTakers.push(cb)

      cb.cancel = once(() => {
        ensureCanMutateNextTakers()
        remove(nextTakers, cb)
      })
    },
    close,
  }
}

multicastChannel返回了一个对象,

  {
    [MULTICAST]: true,
    put(input) {},
    take(cb, matcher = matchers.wildcard) {},
    close:close
  }

从源码看,multicastChannel内部维护了一个nextTakers副作用队列,而currentTakers只是在执行put之后对nextTakers的存储,在对nextTakers进行添加和移除操作时,会比较nextTakers和currentTakers是否相等,如果相等会把nextTakers重置为一个新的对象(开辟新内存),用来确保每次的nextTakers都是更改的,也就是操作的是一个新对象,和currentTakes不是同一个引用。

effectRunner在调用时传入了effect.payload,也就是上面提到的makeEffect方法返回的对象。

channel.take接收两个参数,

第一个是回调函数cb,其实是改造过的next()函数,

第二个是类型判断函数,matcher(pattern)返回的是接收input的类型判断函数,根据当前effect对应的type,从channel的nextTakers取出对应的effect,并执行。

export const string = pattern => input => input.type === String(pattern)

到这里redux-saga的监听原理已经分析完毕,上面的分析比较分散,下面进行一个总结。

核心原理:

在执行runSaga时,会把generator中生成的effect加入channel,在中间件调用时,执行put(action),判断action.type和channel中nextTakers中每一项taker的pattern是否相等,执行对应的taker,执行过后将该taker删除,这也就是说在执行过后需要继续加入taker,这也就是在进行监听时,为什么要把take写在while(true)里面。

下面是take的原理图:

e883064e-b233-eb11-8da9-e4434bdf6706.png

二:redux-saga的设计思想

归根结底还是订阅发布者模式,只是订阅的模式是通过generator的深度自动执行实现的,redux-saga实现了一个自动执行generator的函数。

redux-saga中还有许多其他辅助的逻辑,比如scheduler实现的任务调度等,这里不再深入讨论。

三:redux-saga和dva的关系

完善中

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值