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 的风味。