【源码共读】| 简易实现Redux-Saga

Redux Saga是一个用于管理Redux应用中的副作用(例如异步获取数据,访问浏览器缓存等)的中间件,它的核心是基于ES6的Generator函数实现的。Redux Saga通过创建Saga来组织业务逻辑,使得异步流程控制更加直观,代码更易于测试和调试。如果让我们来实现相关的库,应该怎么去实现呢?

使用

首先,我们以计数器为例,看下saga怎么使用

  • 将项目clone到本地

项目:github.com/redux-saga/...

文档:redux-saga.js.org/docs/introd...

文档中提供了非常详尽的教程,这里不再赘述,这里展示关键步骤

通过上述的案例,我们可以大概猜到了saga的原理:

  • 异步操作被封装在特殊的函数中,这些函数被称为 "sagas"。
  • Saga 通过监听 Redux 的 action,然后根据 action 类型来决定执行哪些异步操作。
  • 通过 dispatch 新的 action 到 Redux store,以触发 state 的更新。

源码分析

通过上面的例子,我们注意到几个核心功能,监听action,处理异步操作,下面我们看下源码中这几个函数

先看下创建的函数

tsx 复制代码
// https://github.com/redux-saga/redux-saga/blob/9c59ac93ec4389d6a1e98e46fc3cddb98523e589/packages/core/src/internal/middleware.js
export default function sagaMiddlewareFactory({
  context = {},
  channel = stdChannel(),
  sagaMonitor,
  ...options
} = {}) {
  let boundRunSaga

  // 校验 channel
  if (process.env.NODE_ENV !== 'production') {
    check(
      channel,
      is.channel,
      'options.channel passed to the Saga middleware is not a channel'
    )
  }

  // sagaMiddleware核心处理
  function sagaMiddleware({ getState, dispatch }) {
    boundRunSaga = runSaga.bind(null, {
      ...options,
      context,
      channel,
      dispatch,
      getState,
      sagaMonitor
    })
    // 返回一个标准的中间件格式
    // next 为 store.dispatch
    // action 为 dispatch(action) 传入的 action
    return (next) => (action) => {
      if (sagaMonitor && sagaMonitor.actionDispatched) {
        sagaMonitor.actionDispatched(action)
      }
      const result = next(action) // hit reducers
      channel.put(action)
      return result
    }
  }

  // 提供一个saga的入口
  sagaMiddleware.run = (...args) => {
    if (process.env.NODE_ENV !== 'production' && !boundRunSaga) {
      throw new Error(
        'Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware'
      )
    }
    return boundRunSaga(...args)
  }
  //  校验 context
  sagaMiddleware.setContext = (props) => {
    if (process.env.NODE_ENV !== 'production') {
      check(props, is.object, createSetContextWarning('sagaMiddleware', props))
    }

    assignWithSymbols(context, props)
  }

  return sagaMiddleware
}
tsx 复制代码
export function runSaga(
  {
    channel = stdChannel(),
    dispatch,
    getState,
    context = {},
    sagaMonitor,
    effectMiddlewares,
    onError = logError
  },
  saga,
  ...args
) {
  // 省略...
  // 校验环境

  // 创建一个迭代器
  const iterator = saga(...args)
  const effectId = nextSagaId()

  // 补全sagaMonitor的方法
  // 省略...
  // 校验环境

  let finalizeRunEffect
  if (effectMiddlewares) {
    // func1(func2(func3(func4))))
    // 从右到左执行
    const middleware = compose(...effectMiddlewares)
    finalizeRunEffect = (runEffect) => {
      return (effect, effectId, currCb) => {
        const plainRunEffect = (eff) => runEffect(eff, effectId, currCb)
        return middleware(plainRunEffect)(effect)
      }
    }
  } else {
    finalizeRunEffect = identity
  }
  // 配置环境变量,用于proc函数
  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
  })
}

可以看到源码中,提供了一个创建saga的接口,将创建好的sagas(处理异步的流程)传入,生成对应的迭代器,通过proc函数来执行generatornext函数,然后将最终的执行结果派发出去,从而实现异步操作的数据更新。

简易实现

我们通过上述的案例和源码分析,接下来,我们来尝试简易实现

需求分析:

  • 创建迭代器
  • 执行迭代器
  • redux 和 saga之间的通信

effect

  • 创建effect,返回标准的effects对象

github.com/redux-saga/...

tsx 复制代码
// https://github.com/redux-saga/redux-saga/blob/main/packages/core/src/internal/io.js
import effectTypes from './effectTypes'
import { IO } from './symbol'

function makeEffect(type, payload) {
  return {
    [IO]: IO,
    type,
    payload
  }
}

export function take(pattern) {
  return makeEffect(effectTypes.TAKE, { pattern })
}

export function put(action) {
  return makeEffect(effectTypes.PUT, { action })
}

export function call(fn, ...args) {
  return makeEffect(effectTypes.CALL, { fn, args })
}

export function fork(fn, ...args) {
  return makeEffect(effectTypes.FORK, { fn, args })
}

export function all(effects) {
  return makeEffect(effectTypes.ALL, { effects })
}
tsx 复制代码
// https://github.com/redux-saga/redux-saga/blob/main/packages/core/src/internal/effectTypes.js
const effectTypes = {
  TAKE: 'TAKE',
  PUT: 'PUT',
  ALL: 'ALL',
  RACE: 'RACE',
  CALL: 'CALL',
  CPS: 'CPS',
  FORK: 'FORK',
  JOIN: 'JOIN',
  CANCEL: 'CANCEL',
  SELECT: 'SELECT',
  ACTION_CHANNEL: 'ACTION_CHANNEL',
  CANCELLED: 'CANCELLED',
  FLUSH: 'FLUSH',
  GET_CONTEXT: 'GET_CONTEXT',
  SET_CONTEXT: 'SET_CONTEXT'
}
export default effectTypes

createSagaMiddleware

这个函数主要做两个功能

  • 与redux通信,触发更新
  • 当action变化时,触发对应的iterator 函数
tsx 复制代码
import runSaga from './runSaga'
import { stdChannel } from './channel'

export default function createSagaMiddleware() {
  // 与 redux 通信的 channel
  let channel = stdChannel()
  let boundRunSaga
  function sagaMiddleware({ getState, dispatch }) {
    // 为什么要用 bind ?
    // 因为在 runSaga 中,需要用到 getState 和 dispatch
    // 但是在这里,还没有创建 store,所以 getState 和 dispatch 还没有
    // 所以这里用 bind 先把 getState 和 dispatch 绑定到 runSaga 上
    // 等到 store 创建好了,再执行 runSaga
    boundRunSaga = runSaga.bind(null, { getState, dispatch, channel })
    // 这里返回的函数,就是 applyMiddleware 中的 next
    return (next) => (action) => {
      let result = next(action)
      // dispatch
      channel.put(action)
      return result
    }
  }

  // 提供给外部的 run 方法
  sagaMiddleware.run = (...args) => {
    return boundRunSaga(...args)
  }
  return sagaMiddleware
}
  • 建立一个通道,暴露出订阅和发布的接口
tsx 复制代码
// 导入MATCH符号,这是一个唯一的标识符,用于在回调函数上存储匹配器函数
import { MATCH } from './symbol'

// 定义一个标准通道
export function stdChannel() {
  // 创建一个数组来存储当前的taker函数
  const currentTakers = []

  // 定义一个take函数,它接受一个回调函数和一个匹配器函数
  // 它将匹配器函数存储在回调函数的MATCH属性上,然后将回调函数添加到taker数组中
  function take(cb, matcher) {
    // cb['@@redux-saga/MATCH'] = matcher
    cb[MATCH] = matcher
    currentTakers.push(cb)
  }

  // 定义一个put函数,它接受一个输入
  // 它遍历taker数组,如果taker的匹配器函数返回true,那么就调用taker函数并传入输入
  function put(input) {
    const takers = currentTakers
    // takers.length 因为在执行taker函数的时候,takers数组的长度可能会发生变化
    for (let i = 0, len = takers.length; i < len; i++) {
      const taker = takers[i]
      if (taker[MATCH](input)) {
        taker(input)
      }
    }
  }

  // 返回一个对象,包含take和put函数
  return {
    take,
    put
  }
}
  • 生成迭代器
tsx 复制代码
import proc from './proc'

export default function runSaga(
  { getState, dispatch, channel },
  saga,
  ...args
) {
  // saga 是一个 generator 函数
  const iterator = saga(...args)
  // 执行 iterator
  proc({ getState, dispatch, channel }, iterator)
}

处理effects

tsx 复制代码
import { IO } from './symbol'
import effectRunnerMap from './effectRunnerMap'

export default function proc(env, iterator) {
  next()

  function next(arg, isErr) {
    let result
    if (isErr) {
      result = iterator.throw(arg)
    } else {
      result = iterator.next(arg)
    }
    if (!result.done) {
      digestEffect(result.value, next)
    }
  }

  function digestEffect(effect, cb) {
    let effectSettled

    function currentCb(res, isErr) {
      if (effectSettled) return
      effectSettled = true
      cb(res, isErr)
    }
    runEffect(effect, currentCb)
  }
  function runEffect(effect, currentCb) {
    if (effect && effect[IO]) {
      // 执行对应的 effect
      const effectRunner = effectRunnerMap[effect.type]

      effectRunner(env, effect.payload, currentCb)
    } else {
      currentCb()
    }
  }
}

effectRunnerMap

tsx 复制代码
import effectTypes from './effectTypes'
import proc from './proc'

function runTakeEffect(env, { channel = env.channel, pattern }, cb) {
  console.log('runTakeEffect')
  const matcher = (input) => input.type === pattern
  channel.take(cb, matcher)
}

function runPutEffect(env, { action }, cb) {
  console.log('runPutEffect')
  const result = env.dispatch(action)
  cb(result)
}

function runCallEffect(env, { fn, args }, cb) {
  console.log('runCallEffect')

  const result = fn.apply(null, args)
  if (result && typeof result.then === 'function') {
    result.then(
      (res) => cb(res),
      (err) => cb(err, true)
    )
    return
  }
  cb(result)
}

// fork 会创建一个新的子进程,子进程会和父进程并行执行
// 非阻塞
function runForkEffect(env, { fn, args }, cb) {
  console.log('runForkEffect')
  const taskIterator = fn.apply(null, args)
  proc(env, taskIterator)
  cb()
}

function runAllEffect(env, { effects }, cb) {
  let n = effects.length
  for (let i = 0; i < n; i++) {
    proc(env, effects[i])
  }
}

const effectRunnerMap = {
  [effectTypes.TAKE]: runTakeEffect,
  [effectTypes.PUT]: runPutEffect,
  [effectTypes.CALL]: runCallEffect,
  [effectTypes.FORK]: runForkEffect,
  [effectTypes.ALL]: runAllEffect
}

export default effectRunnerMap

总结

Redux Saga使用ES6的Generator函数来管理异步操作,使得异步流程更易于读取、写入和测试。

简易实现的核心代码主要是创建一个Generator函数的迭代器,并通过处理迭代器的结果来进行异步操作的管理。

通过建立channel向effect暴露订阅和发布接口,从而实现effects中触发数据更新。

相关推荐
编程零零七2 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
(⊙o⊙)~哦4 小时前
JavaScript substring() 方法
前端
无心使然云中漫步5 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者5 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_5 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
麒麟而非淇淋6 小时前
AJAX 入门 day1
前端·javascript·ajax
2401_858120536 小时前
深入理解MATLAB中的事件处理机制
前端·javascript·matlab
阿树梢6 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写8 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js
史努比.8 小时前
redis群集三种模式:主从复制、哨兵、集群
前端·bootstrap·html