XXHolic / blog

I wonder how~, I wonder why~

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Read Redux-Saga

XXHolic opened this issue · comments

目录

引子

看了下 redux-saga 源码,整理一下个人理解。

源码版本 1.1.3

简介

redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的库,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更方便。redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。目前中文文档跟英文并不是完全同步,但可以对照当做参考。中文文档见这里,英文文档见这里

redux-saga 是 redux 的一个插件,先理解 redux 的基本原理,有助于理解 redux-saga 的部分逻辑。关于 redux 的解读可以参考之前的这篇文章

下面结合官方文档中的计数器例子,对 redux-saga 的一个运作方式做一个概述。需要注意的是,这个例子只是一个半成品,需要按照文档中的引导说明,添加后续的逻辑。这个是个人尝试的

源码

计数器示例

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducers';
import {watchIncrementAsync} from './sagas';
import App from './App';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer,applyMiddleware(sagaMiddleware));
sagaMiddleware.run(watchIncrementAsync);

function render() {
  ReactDOM.render(
    <App {...store} />,
    document.getElementById('root')
  );
}

render();

store.subscribe(render);

reducers.js

export default function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'INCREMENT_IF_ODD':
      return (state % 2 !== 0) ? state + 1 : state
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

sagas.js

import { put, takeEvery } from 'redux-saga/effects'

const delay = (ms) => new Promise(res => setTimeout(res, ms))

export function* incrementAsync() {
  yield delay(3000)
  yield put({ type: 'INCREMENT' })
}

export function* watchIncrementAsync() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}

App.js

import React from 'react';
import Counter from './component/counter'

function App(props) {
  const {getState,dispatch} = props;

  return (
    <div>
      <Counter
        value={getState()}
        onIncrement={() => dispatch({type:'INCREMENT'})}
        onDecrement={() => dispatch({type:'DECREMENT'})}
        onIncrementAsync={() => dispatch({type:'INCREMENT_ASYNC'})}
      />
    </div>
  );
}

export default App;

Counter

import React from 'react'

const Counter = ({ value, onIncrement, onDecrement,onIncrementAsync }) =>
      <div>
        <button onClick={onIncrementAsync}>
          Increment after 3 second
        </button>
        {' '}
        <button onClick={onIncrement}>
          Increment
        </button>
        {' '}
        <button onClick={onDecrement}>
          Decrement
        </button>
        <hr />
        <div>
          Clicked: {value} times
        </div>
      </div>

export default Counter

初始化

在 redux 初始化前,使用 createSagaMiddleware 方法创建了中间件,所在源文件为 middleware.js 。主要逻辑代码如下:

import { assignWithSymbols } from './utils'
import { stdChannel } from './channel'
import { runSaga } from './runSaga'

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)
  }
  // 扩张上下文
  sagaMiddleware.setContext = props => {
    assignWithSymbols(context, props)
  }

  return sagaMiddleware
}

可以看到返回了符合 redux 中间件格式的函数,在 redux 初始化执行的函数是这样的:

applyMiddleware(sagaMiddleware)(createStore)(reducer, preloadedState)

// 将其展开得到 redux 的 dispatch 值
dispatch = action => {
  if (sagaMonitor && sagaMonitor.actionDispatched) {
    sagaMonitor.actionDispatched(action)
  }
  const result = next(action) // hit reducers
  channel.put(action)
  return result
}

这样就跟 redux 原有的对应关系保持了一致。

接着在 redux 初始化之后,执行了中间件自带的 run 方法,且传入了专门用来处理副作用的方法。实际执行方法所在源文件为 runSaga.js 。主要逻辑代码如下:

import { compose } from 'redux'
import proc from './proc'
import { stdChannel } from './channel'
import { immediately } from './scheduler'
import nextSagaId from './uid'
import {, logError, noop, wrapSagaDispatch, identity, getMetaInfo } from './utils'

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 })
  }

  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
  })
}

该方法处理的逻辑有:

  • 监听事件初始化。
  • 其它副作用中间件的处理。
  • 将传入的副作用跟 redux 的 dispatch 合并,并赋给了一个全局变量 SAGA_ACTION 。在计数器示例中,如果不执行这步,将会无法触发对应逻辑。
  • 返回一个 task 对象,里面包含了任务的相关信息和方法。

执行时

在计数器示例中,可以看到无论是不是触发副作用,都统一的使用了 redux 初始化后的 dispatch 的方法。这个也是前面 redux-saga 执行 run 方法的结果。在初始化代码中断点,发现主要的逻辑在下面两句:

  const result = next(action) // hit reducers
  channel.put(action)

触发的时候,首先在 redux 的 redusers 里面匹配是否有对应的 action ,有的话就执行,按照 redux 的逻辑改变 state ;如果没有,则 state 值不变 ,就会到 redux-saga 中进行处理。 channel 对象初始化方法所在源文件为 channel.js 。主要逻辑代码如下:

export function stdChannel() {
  // 初始化对象和方法
  const chan = multicastChannel()
  const { put } = chan
  chan.put = input => {
    // 匹配是否在声明的 sagas 文件内
    if (input[SAGA_ACTION]) {
      put(input)
      return
    }
    /*
    * 放入一个队列中,根据状态决定是否立即执行,
    * 计数器示例加减时虽然也执行了,但在 put 方法中实际上没有匹配到对应的方法,相当于没有执行
    */
    asap(() => {
      put(input)
    })
  }
  return chan
}

小结

通过计数器的例子,可以发现:

  • 副作用的处理统一放入到 saga 文件中。
  • 初始化时,通过自带 run 方法让针对 saga 进行分类,并关联 redux 的 dispatch 方法。
  • 触发时,先使用 take 方法进行注册,然后在 put 中循环匹配对应的方法并执行。

参考资料

🗑️

最近看了《反叛的鲁路修》,这部作品在很早之前就听说过,但曾经看过一次之后,发现机甲打斗蛮多,就没什么兴致看下去。

这么多年后,这次忍着看了下去,发现剧情还是蛮好的。动作摆起来感觉有点 JOJO 的风味。

54-poster