react 状态管理之 redux-saga 的使用与实现原理
上一篇文章# redux middleware 的使用与实现原理介绍了
redux middleware
,今天这篇文章将介绍非常流行的一个用于处理Side Effect
的redux middleware
也就是redux-saga
。
在 redux
的 reducer
是一个纯函数,也就是说同样的输入必须产生同样的输出,不能包含任何的 Side Effect
副作用, redux
副作用是指在redux
应用程序中,除了纯粹的状态变更外,对外部环境产生的任何操作或影响。一些常见的副作用包括:
- 异步操作:例如发送网络请求、读取本地存储或访问数据库等。这些操作需要在某个时刻完成,并且可能会导致状态的变化
- 访问浏览器环境:例如修改
URL
路径、访问浏览器的cookies
或localStorage
等。这些操作与redux
的纯函数原则不符,但是在应用中常常需要进行。 - 访问外部资源:例如获取当前时间、获取设备的地理位置等。这些操作也属于副作用,因为它们不是通过纯函数的方式实现的。
- 使用定时器:比如
settimeout
和setinterval
- 生成随机数或者随机id
副作用不能存在于reducer
中,但是项目中又十分常见所以我们可以用一个库来帮我们处理redux
副作用
什么是 redux-saga
redux-saga
是一个用于管理 redux
副作用的库, 它可以使管理副作用更加容易、执行更高效、测试更简单、在发生错误时能更轻易地定位问题。
redux-saga
使用Generator
生成器,让异步流程的代码更易读,也更容易测试。
如何使用
首先肯定要先安装redux-saga
:
npm install redux-saga
redux-saga
是一个redux
中间件所以要在redux
中使用这个它, 大致代码如下:
import { applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import saga from './effects'
// 通过调用 createSagaMiddlewar 创建 sagaMiddleware
const sagaMiddleware = createSagaMiddleware();
export const store = applyMiddleware(sagaMiddleware)(createStore)(rootReducer);
// 调用 sagaMiddleware 的 run 方法传入根 saga, 这个 saga 是一个 generator
sagaMiddleware.run(saga);
上面的代码通过 redux-saga
默认导出的 createSagaMiddleware
来创建一个 sagaMiddleware
, 接着使用 redux
的 applyMiddleware
方法, 使用这个 sagaMiddleware
。 最后需要做的是 sagaMiddleware.run(saga)
意味着需要执行的一个根 saga
, 接着看下这个根 saga
的内容。
import { take, put } from 'redux-saga/effects';
import { SAGA_INCREMENT, SAGA_DECREMENT } from '.';
function* sagaIncrementWatcher() {
yield take(SAGA_INCREMENT);
yield put(increment(2))
}
function* sagaDecrementWatcher() {
yield take(SAGA_DECREMENT)
yield put(decrement())
}
export default function* saga() {
yield addSagaAddWatcher();
yield addSagaDecrementWatcher();
}
在这个根 saga
中, fork
了两个生成器函数 sagaIncrementWatcher
和 sagaDecrementWatcher
函数, 这两个生成器函数的内容类似, 都是监听某个 action
动作即 take(actionType)
, 在这个动作执行之后会执行后面的内容触发一个 dispatch
改变 state
, 即 put(increment(2))
注意如果 yield 一个迭代器(在这儿是一个生成器) 那么这个迭代器会阻塞saga的执行, take 也会阻塞 saga 的执行, 所以在上面这个例子中, 没有 SAGA_INCREMENT 这个动作 dispatch 之前 sagaDecrementWatcher 是不会被触发的。
在组件中就可以这么使用:
import { store } from '.'
const handleSagaIncrement = () => {
store.dispatch({ type: SAGA_INCREMENT })
}
const handleSagaDecrement = () => {
store.dispatch({ type: SAGA_DECREMENT })
}
在组件中同样是通过 store.dispatch
一个 action
来触发 saga
中 take
的 actionType
, 从而执行后面的逻辑!
上面代码中的 take
, put
被称为 effect creators
, 这些 effect creators
执行之后实际上就是返回一些简单的 javascript
对象, 我们称这些返回对象为 Effect
, 只不过通过 yield Effect
可以处理不同的逻辑,下面介绍一些常用的 effect creators
take
take
用于指定 saga
等待某一 action
触发, 只有匹配的 action
派发之后才继续执行, 否则将暂停 Generator
函数的执行, 通俗来说就是会阻塞 saga
运行。
take
的参数可以是一个字符串、或者一个函数、或者字符串和函数的数组。传入字符串*
时将会匹配所有action
, 看下面的例子:
function* watchSaga() {
// 等待 action { type: 'add' } 被派发, 被派发之后才会继续执行, 否则将暂停执行
yield take('add');
console.log('add action has been dispatched');
}
take
也可以接收一个channel
, 这在saga
中处理事件监听非常有用, 比如在saga
中处理websocket
消息的收发等, 详见官方文档Channels
fork(fn, ...args)
以非阻塞的方式执行函数 fn
, fn
可以是一个 Generator
函数或者一个返回 Promise
的普通函数, args
即是向 fn
传递的参数
takeEvery
使用方式和 take
差不多, 只不过 takeEvery
会响应多次 action
而 take
只会执行一次, 并且 takeEvery 是非阻塞的
实际上
takeEvery
内部就是通过fork
和不断地调用take
来实现的
put
用于在 saga
中派发动作, 使用方法和 dispatch
一样, 非阻塞
例如派发一个 add
的 action
:
function* foo(){
yield put({
type: 'add',
payload: 3,
});
}
call(fn, ...args)
以 args
作为参数调用 fn
, fn
可以是一个 Generator
函数或者是一个返回Promise
的函数或者其他普通函数。call
会阻塞saga
的执行。
select(selector, ...args)
用于通过selector
函数获取state
中的计算值, args
是传给selector
函数的可选参数
all([...effects])
并行地运行多个 Effect
,并等待它们全部完成, 类似于 Promise.all
redux-saga 拥有众多的 effect-creator, 可以通过 redux-saga api 查看。
原理及实现
前置知识
- function* 生成器函数,
function\*
这种声明方式(function关键字后跟一个星号)会定义一个生成器函数 (generator function
),它返回一个Generator
对象。 - Generator 对象 生成器对象是由一个
generator function
返回的,并且它符合可迭代协议和迭代器协议。拥有三个方法:- Generator.prototype.next(value) 返回一个拥有
done
和value
的对象,如果done
为true
表示迭代完毕,反之亦然;value
是在generator
中被yield
的值或者generator
函数的返回值。可以给next
方法传递参数, 这个参数将会称为yield
表达式的值。 - Generator.prototype.return() 返回给定的值并结束生成器。
- Generator.prototype.throw() 向生成器抛出一个错误。
- Generator.prototype.next(value) 返回一个拥有
- co 函数, 自动地迭代一个 Generator 对象。
- 判断一个对象是否是迭代器,拥有
Symbol.iterator
属性的对象就是一个可迭代的对象,Symbol.iterator
通常是一个返回迭代器的函数。
通过 co
函数可以自动的迭代一个生成器函数,一个简单的 co
函数如下:
function co(generator) {
const it = generator();
function next(iterator) {
const { value, done } = iterator.next();
if (done) return value;
return next(iterator);
}
return next(it);
}
实现
redux-saga
在代码健壮性上做了很多工作比如错误处理, 调度处理等, 在功能上由各种的 effectCreator 来实现。 所以以下代码只是核心原理, 并选择了一些effectCrator
来实现, 并不是源码的一比一还原。
首先 redux-saga
导出一个默认方法 createSagaMiddleware
, 在 createSagaMiddleware
方法中会创建一个中间件 sagaMiddleware
, 这个 sagaMiddleware
中初始化了一个 boundRunSaga
方法, 这个方法是通过给 runSaga
绑定第一个参数 { channel, getState, dispatch }
后返回的一个新方法, 所以在 runSaga
内部能访问这个 { channel, getState, dispatch }
等一些列对象。
其中给 runSaga
绑定的参数对象属性的作用如下:
channel
主要用于发布订阅, 在redux
中一个action
被dispatch
时通知监听该action
的监听方法触发, 在redux
中take effect
会监听action
的dispatch
, 在redux-saga
中自己实现了一套发布订阅, 这里我用比较熟悉的events
包, 它的API
和node
中的events
模块一致。getState
, 这是通过redux
传递进来的参数, 可以获得store
中的state
dispatch
, 同样是通过redux
传递进来的参数, 可以派发动作
import runSaga from './runSaga';
import EventEmitter from 'events';
function createSagaMiddleware() {
// 创建一个 channel 用于在 redux 的某个 action 被派发时通知监听函数
const channel = new EventEmitter();
let boundRunSaga;
function sagaMiddleware(store) {
const { getState, dispatch } = store;
boundRunSaga = runSaga.bind(null, { channel, getState, dispatch })
return function (next) {
return function (action) {
const result = next(action);
channel.emit(action.type, action);
return result;
}
}
}
sagaMiddleware.run = (saga) => boundRunSaga(saga);
return sagaMiddleware;
}
export default createSagaMiddleware;
所以可以看到我们在使用时调用的 sagaMiddleware.run(rootSaga)
其实就是这个 boundRunSaga
, 本质上就是 runSaga
:
import * as effectTypes from './effectTypes';
const CANCEL_TASK = 'CANCEL_TASK'; // 用于取消 saga 执行
export default function runSaga(env, saga, cb) {
// task 最终会被返回, 调用 task.cancel 可以取消 saga 的执行
const task = {
cancel: () => { next(CANCEL_TASK); },
};
const { channel, dispatch } = env;
const it = typeof saga === 'function' ? saga() : saga;
// saga 本质上就是不断地迭代 Generator 使其迭代完成, 在迭代的过程中处理不同的 Effect
function next(value) {
const { value: effect, done } = value === CANCEL_TASK
? it.return(value) // 如果派发了 CANCEL_TASK 那么取消执行
: it.next(value);
if (!done) {
if (typeof effect[Symbol.iterator] === 'function') {
// 如果迭代的是一个 Generator 那么会调用 runSaga 迭代这个 Generator, 迭代完毕之后才会调用 next
// 所以在 saga 中 yield generatorFn() 是阻塞的
runSaga(env, effect, next);
} else if (effect instanceof Promise) {
// 如果迭代的是一个 Promise, 比如 yield fooPromise(), 那么会在这个 promise resolve 之后再继续迭代
// 所以 yield fooPromise() 也是阻塞的
effect.then(next);
} else {
// 接下来就是处理 saga 中的各种 effect creators 的返回值
switch (effect.type) {
case effectTypes.TAKE:
// 处理 take, 可以看到使用的是 channel.once 也就是说 take 只会响应一个 action
// 并且 take 是阻塞的, 只有在匹配的 action 被派发之后才会继续迭代
channel.once(effect.actionType, next);
break;
case effectTypes.PUT:
// 处理 put, 实际上就是调用 dispatch
dispatch(effect.action);
next(effect.action);
break;
case effectTypes.FORK:
// 处理 fork, 直接调用 runSage 创建一个新的saga, 可以看到 fork 是非阻塞的
const forkTask = runSaga(env, effect.saga);
// 这里意味着 yield fork(fooSaga) 的表达式的值就是这个 task
// 比如 const someTask = yield fork(fooSaga); 此时便可以通过 someTask.cancel() 来取消这个task
next(forkTask);
break;
case effectTypes.CALL:
// 处理 call, 用 Promise.resolve 来包裹 fn 的执行结果, 如果 fn 返回一个 promise 那么将会将该 promise resolve 的值传递给 Promise.resolve
Promise.resolve(effect.fn(...effect.args))
.then(next)
break;
case effectTypes.ALL:
// 处理 all, 实现思路和 Promise.all 差不多, 用一个计数器来记录迭代完的 Effect 在全部 Effect 迭代结束之后, 调用 next
const result = [];
let count = 0;
effect.iterators.forEach((iterator, index) => {
function complete(val) {
count += 1;
result[index] = val;
if (count === effect.iterators.length) {
next(result);
}
}
if (typeof iterator[Symbol.iterator] === 'function') {
// 如果 Effect 是一个 generator 那么调用 runSaga
runSaga(env, iterator, complete);
} else {
Promise.resolve(iterator.fn(...iterator.args)).then(complete);
}
});
break;
case effectTypes.SELECT:
// 处理 select
next(effect.selector(getState(), ...effect.args));
break;
case effectTypes.CANCEL:
effect.task.cancel();
next();
break;
default:
break;
}
}
} else {
// 这类的 cb 在处理 all 时用到
cb && cb(effect);
}
}
next();
return task;
}
分析以上代码 runSaga
方法其实就是一个自动迭代生成器的这么一个方法, 它接受三个参数一个是 env
, 也就是在 createSagaMiddleware
中 bind
的那个参数{ channel, getState, dispatch }
。第二个参数是一个 saga
, 可以是一个生成器函数也可以是一个迭代器。 第三个参数是一个 callback
, 用于在迭代完成之后的回调。其中在每次迭代时 effect
就是这个迭代器执行 it.next()
返回的 value
值, effect 的类型可能是这些:
effect
可能是一个迭代器, 比如generate
函数的执行结果, 如果是一个迭代器的话那么我们会将这个这个迭代器再使用runSaga
进行迭代, 并且将next
方法传入, 在effect
迭代完成之后会执行next
方法, 那么此时就能继续迭代saga
了effect
可能是一个Promise
, 那么在Promise.then
方法中传入next
, 使得在Promise resolve
之后能继续迭代effect
可能是通过内部方法创建的对象, 那么就要根据不同的effect
对象处理不同的逻辑
针对不同的 effect
进行了相应的处理, effect
就是我们使用的从 redux-saga/effects
中导入的函数, 比如 take, put, fork, call
等。在内部这些方法就是返回一些对象, 最终给到 runSaga
函数来处理。
其中 effect
对象的 type
有这么一些值:
export const TAKE = 'TAKE'; // 对应 take 方法
export const PUT = 'PUT'; // 对应 put 方法
export const FORK = 'FORK'; // 对应 fork 方法
export const CALL = 'CALL'; // 对应 call 方法
export const ALL = 'ALL'; // 对应 all 方法
export const SELECT = 'SELECT'; // 对应 select 方法
export const CANCEL = 'CANCEL'; // 对应 cancel 方法
各种 effect 方法:
import * as effectTypes from './effectTypes';
export function take(actionType) {
return { type: effectTypes.TAKE, actionType }
}
export function put(action) {
return { type: effectTypes.PUT, action }
}
export function fork(saga, ...args) {
return { type: effectTypes.FORK, saga: saga.bind(null, ...args) }
}
export function takeEvery(actionType, saga) {
function* takeEveryHelper() {
while (true) {
const action = yield take(actionType);
yield fork(saga, action); // saga 是一个生成器函数, 所以使用 fork 来 run 这个生成器函数
}
}
return fork(takeEveryHelper);
}
export function call(fn, ...args) {
return {
type: effectTypes.CALL, fn, args
}
}
export function all(iterators) {
return {
type: effectTypes.ALL, iterators
}
}
export function cancel(task) {
return {
type: effectTypes.CANCEL, task
}
}
export function delay(ms) {
return new Promise(r => setTimeout(() => {
r()
}, ms))
}
结合以上代码我们具体分析一下各个 effect creator
的实现
take effect creator 实现
当我们调用 take
函数时, 会传入 actionType
, 也就是 dispatch(action)
中的 action.type
, 在 runSaga
中会调用 channel
的 once
函数来监听对应的 dispatch(action)
动作, 在 sagaMiddleware
中间件中会通过 channel.emit(action.type, action)
来触发这个监听函数, 在监听函数中会调用 next
函数从而继续迭代 saga
。
put effect creator 实现
在调用 put
函数时, 会传入对应的 action
, 在 runSaga
中可以通过 env
对象拿到 dispatch
函数从而去改变 store
中的 state
fork effect creator 实现
fork
接收一个生成器函数 fn
作为第一个参数, 后面的可以传递任意的参数, 将会作为 fn
的参数进行调用。在 runSaga
中迭代到 fork
时会将这个 fn
作为参数传递给 runSaga
, 并且迭代这个 fn
生成器函数并不会阻塞当前的 saga
执行。当前的 saga
继续迭代。
call effect creator 实现
call
接收函数 fn
作为第一个参数, 该函数可以是一个返回 promise
的函数也可以是一个普通函数, 后面的可以传递任意的参数, 将会作为 fn
的参数进行调用。 在 runSaga
中如果迭代到 call
时, 会调用 fn
并传入对应的参数, 并且用 Promise.resolve
包裹 fn
的返回结果, 这样无论 fn
的返回结果是不是一个 Promise
都会包装成一个 Promise
, 并且在该 Promise
的 then
方法中调用 next
继续迭代 saga
。
takeEvery effect creator 实现
takeEvery
接收两个参数, 第一个参数是需要监听的 action.type
, 第二个参数是一个生成器函数 workerSaga
用于处理监听到的 action.type
。takeEvery
内部使用 fork
和 take
来实现, 首先返回一个 fork
以非阻塞的方式迭代 takeEveryHelper
, takeEveryHelper
通过 take
来监控 actiontType
, 并再次通过 fork
来迭代 workerSaga
。
function takeEvery(actionType, saga) {
function* takeEveryHelper() {
while (true) {
const action = yield take(actionType);
yield fork(saga, action); // saga 是一个生成器函数, 所以使用 fork 来 run 这个生成器函数
}
}
return fork(takeEveryHelper);
}
all effect creator 实现
all
接收一个数组, 该数组的成员可以是迭代器, 也可以是通过 call
函数返回的对象, 类似于 Promise.all
, all
函数会在所有的成员并发地进行迭代, 并且在每个成员迭代完之后将迭代的结果返回。在 runSaga
中会遍历数组中的成员, 并使用一个指针 count
来记录当前迭代完成的数量, 在所有的成员都迭代完成之后会调用 next
, 继续迭代 saga
。并且在迭代成员时做了一个判断, 如果是迭代器那么用 runSaga
方法来进行迭代, 并且传入 complete
函数作为 callback
; 如果是一个 Promise
那么在 Promise.then
方法中调用 next
select effect creator 实现
select
接收一个 selector
作为第一个参数,后面的可以传递任意的参数,将作为 selector
函数的第二个参数及第二个参数后面的参数。在 runSaga
中如果迭代到了 select
则会将 env
中 getState()
执行结果及 select
传入的参数作为 selector
的参数执行,最后调用 next
方法传入得到的结果,这样就能在 yield
左侧拿到查询结果了
cancel effect creator 实现
在 fork
或者 takeEvery
调用之后会返回一个 task
对象,调用 cancel(task)
函数可以取消执行该任务。所以 runSaga
函数需要返回一个 task
对象,该对象拥有一个 cancel
方法,这个 cancel
方法执行后会调用 next
函数,并传入一个 CANCEL_TASK
常量,在 next
函数中判断如果传入的 value
是 CANCEL_TASK
那么就调用 it.return()
将这个迭代器迭代完毕,之后就不再迭代了。并且在 fork
这个 effect
中将 runSaga
的返回对象 task
作为参数传递给 next
方法,最终 yield fork()
的值就是这个 task
了。而在 runSaga
中如果 effect
是 CANCEL
的话直接调用 task.cancel()
总结
以上就是 redux-saga
的使用与实现原理的全部内容, 篇幅有限文中还有很多 redux-saga
的 API
没有讲到, 纸上得来终觉浅,绝知此事要躬行, 还需要大家自己在项目中学习和总结。
转载自:https://juejin.cn/post/7243725129461366840