Redux Saga是一个用于管理Redux应用中的副作用(例如异步获取数据,访问浏览器缓存等)的中间件,它的核心是基于ES6的Generator函数实现的。Redux Saga通过创建Saga来组织业务逻辑,使得异步流程控制更加直观,代码更易于测试和调试。如果让我们来实现相关的库,应该怎么去实现呢?
使用
首先,我们以计数器为例,看下saga怎么使用
- 将项目clone到本地
文档: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
函数来执行generator
的next
函数,然后将最终的执行结果派发出去,从而实现异步操作的数据更新。
简易实现
我们通过上述的案例和源码分析,接下来,我们来尝试简易实现
需求分析:
effect
- 创建effect,返回标准的effects对象
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中触发数据更新。