前面几篇文章讲了redux react-redux 今天就来讲讲redu-sage,为什么要单独拿这个中间件来说呢?想必大家都知道,因为这个中间件很普遍,对于我们在redux或者react-redux中处理异步请求以及副作用,简单的异步我们可以是用redux-thunk,也是可以完成,但是对于比较复杂的情况saga应付起来就比较容易,也不易发生回调地狱!
概念
redux-saga
是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。
前置知识
中文文档:https://redux-saga-in-chinese...
英文文档:https://redux-saga.js.org/
学习saga是有前提条件的如果以下知识点还不太清楚,那学起来可能会比较吃力,建议先行学习;
基本属性以及实现
effect
概念:在 redux-saga
的世界里,Sagas 都用 Generator 函数实现。我们从 Generator 里 yield 纯 JavaScript 对象以表达 Saga 逻辑。 我们称呼那些对象为 Effect
注意下述代码中Interface
代表接口的意思,payload
代表参数,大写的英文代表指令;
call
阻塞调用saga,只有call调用的saga有结果返回以后代码才会继续执行;
使用:
yield call(Interface, payload);
fork
非阻塞调用saga,无需等待fork调用的saga代码继续执行;
yield fork(Interface, payload);
all
阻塞调用可同时调用多个saga,类似于promise.all;
yield all([
Interface(payload),
Interface1(payload1),
]);
take
take创建一个命令对象,告诉middleware等待redux dipatch匹配的某个pattern的action;
const action = yield take(PATTERN);
put
这个函数用于创建dispatchEffect,可以修改redux store中的状态,其实就是redux中dispatch的封装
yield put({type: ACTION, payload: payload});
以上几个effect 源码实现起来比较简单,其实就是进行一个简单的标记,告诉后续的程序我这里是什么操作而已!就直接贴核心原理代码了。
import effectTypes from "./effectTypes";
import { IO } form "./symbols";
// 标记操作类型
const makeEffect = (type, payload) => ({ [IO]: IO, type, payload });
export function take(pattern) {
return makeEffect(effectTypes.TAKE, { pattern })
}
export function put(action) {
return makeEffect(effectTypes.PUT, { action })
}
// call的fn是一个promise
export function call(fn, ...arg) {
return makeEffect(effectTypes.CALL, { fn, arg })
}
// fork的fn是一个generator函数
export function fork(fn, ...arg) {
return makeEffect(effectTypes.FORK, { fn, arg })
}
// all的fns是一个promise组成的数组
export function all(fns) {
return makeEffect(effectTypes.ALL, fns)
}
两个标记常量的文件这里直接给源码的地址吧!
createSagaMiddleware
源码中处理createSagaMiddleware
这个逻辑的函数名叫sagaMiddlewareFactory
import { stdChannel } from './channel';
import runSaga from './runSaga';
export default function createSagaMiddleware() {
let boundRunSaga;
// 因为需要比对actiony和pattern,需要保证使用的是一个channel所以在这里初始化一次channel即可
let channel = stdChannel()
// 根据redux 的middleware对于中间件的处理我们可以了解这里热入参是getStore, dispatch
// 并且返回一个next => action => next(action)的函数,不了解的小伙伴可以去翻看下我之前写的redux的middleware的源码
function sagaMiddleware({ getStore, dispatch }) {
// 因为我们希望runSaga可以获取到store的控制权,并且接收sagaMiddleware.run函数的参数,所以我们
// 在这里用bind缓存赋值给boundRunSaga,并将控制权函数传入,因为不需要改变作用域所以第一个参数为null
boundRunSaga = runSaga.bind(null, { channel, getStore, dispatch })
return next => action => {
const result = next(action)
channel.put(action)
return result
}
}
sagaMiddleware.run = (...args) => boundRunSaga(...args)
return sagaMiddleware
}
runSaga
import proc from "./proc"
export default function runSaga({ channel, getStore, disparch }, saga, ...args) {
// 这个saga就是generator方法,我们需要执行才能获取到遍历器对象
// 我们需要拿到遍历器对象才能拿到里面的状态,执行里面的effect
// 这步骤我们需要我们替用户操作
const iterator = saga(args)
// 根据generator惰性求值的特点,我们单独声明一个文件(proc)去处理generator的next方法
// proc需要处理的是遍历器对象,以及过程中需要修改状态所以需要{ getStore, disparch }, iterator作为参数
const env = { channel, getStore, disparch }
proc(env, iterator)
}
proc
功能:接受runSaga传递过来的遍历器对象,调用遍历器对象的next函数,并且以及effect的标记调用effectRunnerMap中对应的函数
import effectRunnerMap from "./effectRunnerMap";
import { IO } form "./symbols";
export default function proc(env, iterator, cb) {
// 这里面我们需要处理next函数,所以我们需要自己定义下next
// 首次调用是不需要参数的
next();
function next(arg, isErr) {
let result;
// 执行中我们需要判断是否存在错误,确定无错误的时候才正常执行遍历器对象的next函数
if (isErr) {
// 在这里的arg是具体的错误信息
result = iterator.throw(arg)
}
else {
result.next(arg)
}
// result {value, done: true/false}
// 如果done为fasle,说明遍历未结束,需要继续遍历
if (!result.done) {
digesEffect(result.value, next)
}
else {
// 遍历结束
if (cb && typeof cb === "function") {
cb(result)
}
}
}
function runEffect(effect, currCb) {
// 判断这里的effect方法是不是saga内部定义的
if (effect && effect[IO]) {
// 根据标记获取对应的方法
const effectRunner = effectRunnerMap[effect.type]
effectRunner(env, effect.payload, currCb)
}
else {
// 如果不是内部定义的effect,则直接执行currCb,进行下一次next
currCb()
}
}
// 我们需要在digesEffect在处理具体的effect比如take/put/call等等
function digesEffect(effect, cb) {
// 在这里我们需要判断一下effect的执行状态如果执行结束就不需要重复执行
let effectSettled;
function currCb(res, isErr) {
if (effectSettled) {
return
}
effectSettled = true
cb(res, isErr)
}
runEffect(effect, currCb)
}
}
effectRunnerMap
功能:这里存放的是take、call等副作用的具体处理逻辑包括修改store中state的操作
import effectTypes from './effectTypes'
import proc from "./proc"
import { promise, iterator } from './is'
// 这个文件主要是和effect方法中的标记相对应根据当时标记获取这里对应的方法
// channel 这样获取是因为源码中的take是可以接受外界传进来的channel的,默认使用env当中的
function runTakeEffect(env, { channel = env.channel, pattern }, cb) {
// 我们只有发起一次dispatch拿到对应的pattern
//并且pattern和dispatch的action匹配上才会去执行cb
const matcher = input => input.type === pattern;
// 匹配以后我们需要把cb 和 pattern关联以后保存起来等待dispatch之后调用
// 所以我们声明一个channel来保存
channel.take(cb, matcher)
}
function runPutEffect(env, { action }, cb) {
// put 其实就是修改store中的state的过程,所以直接执行dispatch就可以了,
// 同样的我们执行只有继续调用cb,并把dispatch的执行结果返回
const result = env.dispatch(action)
cb(result)
}
function runCallEffect(env, { fn, args }, cb) {
// call这里的fn可能是promise,也可能是generator函数,也可能就是普通函数需要区分
// 源码中专门判断返回的result的类型是不是promise类型,是一个叫is的静态文件
const result = fn.apply(null, args)
// 源码中是调用的resolvePromise函数来判断的,在resolvePromise中引用了is文件
if (promise(result)) {
// 在then中回调cb
result.then(resp => cb(resp)).catch(error => cb(error, true))
return
}
// iterator也是从is静态文件取出来的
if (iterator(result)) {
// 在proc函数上加一个新的参数,目的是在遍历器结果done为true的时候才去执行cb从而达到阻塞的效果
proc(env, result, cb)
return
}
// 如果是普通函数的我们直接调用cb
cb(result)
}
function runForkEffect(env, { fn, args }, cb) {
// 先执行fn, fn是generator函数,执行fn先拿到遍历器对象,然后在执行遍历器对象的next
// 所以我们继续交给proc来处理就好了
// 这里需要注意的是啊这个apply,我们之前标记fork函数的时候对args进行了解构,所以这里的args是一个类数组对象
// 而用户调用fork的是传入的第二个参数是payload,所以这里我们其实应该写fn(args[0])才能获取到正确的payload,
// 但是为了更好的兼容,源码中使用了fn.apply(args),利用apply接受一个类数组参数的原理,对参数进行解构
const iterator = fn.apply(args)
proc(env, iterator)
// 处理完成完以后,直接调用cb即可,因为fork是非阻塞的
cb()
}
function runAllEffect(env, fns, cb) {
// 这里的fns是遍历器对象组成的数组,我们遍历这个数组就可以拿到每一个遍历器对象
// 然后继续使用proc文件处理这个遍历器对象
const len = fns.length;
for (let i = 0; i < len; i++) {
proc(env, fns[i])
}
}
const effectRunnerMap = {
[effectTypes.TAKE]: runTakeEffect,
[effectTypes.PUT]: runPutEffect,
[effectTypes.CALL]: runCallEffect,
[effectTypes.FORK]: runForkEffect,
[effectTypes.ALL]: runAllEffect,
}
export default effectRunnerMap
channel
需要在createSagaMiddleware中初始化
我们使用take和put来与redux store进行通信,channel概括了这些effect与外部事件源或sagas之间的通信;
import { MATCH } form "./symbols";
export function stdChannel() {
// 声明一个变量来保存,因为有可能是多个所以使用数组
let currentTakers = [];
function take(cb, matcher) {
cb[MATCH] = matcher
currentTakers.push(cb)
}
function put(input) {
const takers = currentTakers;
// 因为currentTakers是动态变化的如果这里不赋值给len有可能会造成死循环
for (let i = 0, len = takers.length; i < len; i++) {
const taker = takers[i];
if (taker[MATCH](input)) {
taker(input)
}
}
}
return {
take, put
}
}
总结
以上就是一些基础的effect的核心逻辑代码,以及saga整体流程,这里简单做个流程总结:
- 在createSagaMiddleware中初始化channel,并且获取从redux的middleware中释放出来的store的控制权;
- 用bind将runSaga函数重新赋值给sagaMiddleware.run 并追加store的控制权以及经过初始化的channel;
- 在runSaga中获取遍历器对象(iterator),并调用proc文件处理遍历器对象(iterator);
- proc主要负责执行遍历器对象,并通过IO标记和effectRunnerMap具体确认当前遍历器对象主要处理的effect是哪一种,并调用effectRunnerMap中对应的函数进行处理;
个人觉得这个apply和bind也算是一种妙用吧!括弧笑