react篇lesson5(redux-saga)知识点

前面几篇文章讲了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是有前提条件的如果以下知识点还不太清楚,那学起来可能会比较吃力,建议先行学习;
  • generator
  • promise
  • redux
  • redux中间件
  • react
基本属性以及实现 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) }

两个标记常量的文件这里直接给源码的地址吧!
  • symbols文件
  • effectType文件
  • is文件
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 } }

总结 【react篇lesson5(redux-saga)知识点】以上就是一些基础的effect的核心逻辑代码,以及saga整体流程,这里简单做个流程总结:
  1. 在createSagaMiddleware中初始化channel,并且获取从redux的middleware中释放出来的store的控制权;
  2. 用bind将runSaga函数重新赋值给sagaMiddleware.run 并追加store的控制权以及经过初始化的channel;
  3. 在runSaga中获取遍历器对象(iterator),并调用proc文件处理遍历器对象(iterator);
  4. proc主要负责执行遍历器对象,并通过IO标记和effectRunnerMap具体确认当前遍历器对象主要处理的effect是哪一种,并调用effectRunnerMap中对应的函数进行处理;
    个人觉得这个apply和bind也算是一种妙用吧!括弧笑

    推荐阅读