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中的statedispatch, 同样是通过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