Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read Redux-Saga #54

Open
XXHolic opened this issue May 1, 2020 · 0 comments
Open

Read Redux-Saga #54

XXHolic opened this issue May 1, 2020 · 0 comments
Labels

Comments

@XXHolic
Copy link
Owner

XXHolic commented May 1, 2020

目录

引子

看了下 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

@XXHolic XXHolic added the read label May 1, 2020
@XXHolic XXHolic mentioned this issue May 4, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant