高效学习系列之【redux-saga原理解读】

124 阅读6分钟

前言

高质量的学习需要的是耐心【1】和一些技巧【2】

我不是一个按套路出牌的人,我看过一些文章和资料,不乏写的很不错的,但是觉得太教条主义,而我要做的是传授,我想让程序的世界更生动...

这篇文章,更适用于初级,在这里,不仅仅分享的是技术,还有我的学习理解

正文部分

学习思路

面对一个新的事物,不限于汽车,玩具,游戏,家电。我们总是要了解它的用途和功能以及如何使用它。我觉得这种思想在软件世界里一样通用,只不过一切都变得抽象了,不再是眼睛看到的,而是要通过思考来理解。

就拿redux-saga为例,一个libary库?redux中间件?解决了异步问题? 没错,其实也可以换个方式思考,它是个产物,我们可以把redux-saga 等同于一个 汽车,家电这种。

尝试这种思考模式,我们继续

使用方法和用途

学习redux-saga前需掌握generator,如果你还不熟悉可以去看看 阮一峰的文章 前置条件【3】

redux-saga的使用方法和api这里就不讲解,可以参考官方文档 中文 英文 前置条件

如果你已经掌握了redux-saga的用途和使用方法,那我们继续聊,否则会影响接下来的阅读和理解

需求分析

相信你对redux-saga已经掌握很熟练,这里我们再简短介绍下

概述: redux-saga 是⼀个⽤于管理应⽤程序 Side Effect(副作⽤,例如异步获取数据,访问浏览器缓存等)的 library,它的⽬标是让副作⽤管理更容易,执⾏更⾼效,测试更简单,在处理故障时更容易。

接下来,梳理下重点需求:

1.既然是redux的中间件,createSagaMiddleware的返回产物一定符合中间件定义标准,支持run方法 2.Side Effect 副作用如何处理? 3.take、put、call、fork、all... 方法

逐个击破

effect 和 take、put、call、fork、all

源码:

import { IO } from "./symbols";
import * as effectTypes from "./effectTypes";
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 });
}

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);
}

讲解:
IO 只是一个symbol。
effectTypes 副作用类型常量。
makeEffect 副总用对象生成器,类似于redux的action,不同type来区分行为。 take put等方法最终都是生成副作用对象 { [IO]: IO, type, payload }

createSagaMiddleware

import runSaga from "./runSaga";
import { stdChannel } from "./channel";

export default function createSagaMiddleware() {
  let boundRunSaga;
  let channel = stdChannel();
  function sagaMiddleWare({ getState, dispatch }) {
    boundRunSaga = runSaga.bind(null, {
      channel,
      getState,
      dispatch,
    });
    return (next) => (action) => {
      let result = next(action);
      channel.put(action);
      return result;
    };
  }
  sagaMiddleWare.run = (...args) => boundRunSaga(...args);
  return sagaMiddleWare;
}

讲解:
createSagaMiddleware:创建一个符合redux规范的中间件,同时支持run方法。
这里通过闭包返回sagaMiddleWare,创造了单独的作用域空间,避免污染全局。 runSaga 处理run接收的的函数【generator函数】。 boundRunSaga runSaga以及后面的处理需要getState,dispatch,channel,通过runSaga.bind实现。 stdChannel,channel 一种订阅发布空间,后面会讲。

runSaga

import proc from "./proc";

export default function runSaga({channel, dispatch, getState}, saga, ...args) {
  const iterator = saga(...args);
  const env = {
    dispatch,
    getState,
    channel
  }
  proc(env, iterator);
}

讲解:
通过const iterator = saga(...args);执行generator函数,得到一个遍历器对象
调用 proc 【带有effect generator函数过程处理】

proc

import { IO } from "./symbols";
import effectRunnerMap from "./effectRunnermap";

export default function proc(env, iterator, cb) {
  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)
    } else {
      if (typeof cb === 'function') {
        cb(result.value)
      }
    }
  }
  function runEffect(effect, currCb) {
    if (effect && effect[IO]) {
      const effectRunner = effectRunnerMap[effect.type];
      effectRunner(env, effect.payload, currCb)
    } else {
      currCb();
    }
  } 
  function digestEffect(effect, cb) {
    let effectSettled;
    function currCb(res, isErr) {
      if(effectSettled) return;
      effectSettled = true;
      cb(res, isErr);
    }
    runEffect(effect, currCb)
  }
}

讲解:
proc 带有effect的generator函数处理,从我的表述,这个函数是一个独立的功能,在redux-saga里多出调用,包括:runCallEffect,runForkEffect,runAllEffect。
iterator generator函数生成的遍历器对象,需要调用next方法来进行下一步。
next 对iterator.next进行了封装,通过digestEffect->runEffect处理next返回的efffect result = iterator.next(arg)得到的是take,put的返回对象,所以这里判断哪一个来具体执行。const effectRunner = effectRunnerMap[effect.type]

effectRunnermap

import  * as effectTypes from './effectTypes';
import proc from './proc';
import * as is from './is';

function runTakeEffect(env, {channel = env.channel, pattern}, cb) {
  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(is.promise(result)) {
    result.then((data) => {
      cb(data);
    }).catch((err) => {
      cb(err, true);
    })
    return;
  }
  if(is.iterator(result)) {
    proc(env, result, cb);
    return;
  }
  cb(result);
}

function runForkEffect(env, {fn, args}, cb) {
  console.log('runForkEffect');
  const taskIterator = fn.apply(null, args);
  proc(env, taskIterator);
  cb();
}

function runAllEffect(env, effects, cb) {
  console.log('runAllEffect');
  const len = effects.length;
  for (let i = 0; i < len; i++) {
    proc(env, effects[i]);
  }
  cb();
}

const effectRunnerMap = {
  [effectTypes.TAKE]: runTakeEffect,
  [effectTypes.PUT]: runPutEffect,
  [effectTypes.CALL]: runCallEffect,
  [effectTypes.FORK]: runForkEffect,
  [effectTypes.ALL]: runAllEffect,
}

export default effectRunnerMap;

讲解:
通过策略模式的方式,处理不同的内置effect副作用
channel 订阅发布管理器,take注册监听事件
runCallEffect,runForkEffect,runAllEffect支持generator函数回调,内部也是通过proc来处理,此处体现了proc这个函数的抽象独立功能

channel

import { MATCH } from "./symbols";

export function stdChannel() {
  const currentTakers = [];
  function take(cb, matcher) {
    cb[MATCH] = matcher;
    currentTakers.push(cb);
  }
  function put(input) {
    const takers = currentTakers;
    for(let i = 0, len=takers.length; i<len ; i++) {
      const taker = takers[i];
      if(taker[MATCH](input)) {
        taker(input);
      }
    }
  }
  return {
    take,
    put,
  }
}

讲解:
MATCH 通过type来判断是否是注册的监听函数
take 在订阅监听的函数上加上MATCH
put 接收action,通过action.type判断,触发对应函数,在createSagaMiddleware代码中channel.put(action) 调用了put

总结

通过以上的原理讲解,相信你已经对redux-saga的原理有了更多的理解。
对于一些初学者,经验少的工程师,理解源码原理不算是一件容易的事情,只要你有耐心,不断地反思来提示自己的学习技巧,一定会有质的变化
如果你对redux-sage的源码原理足够掌握,这时候你可以和很多知识体系结合起来,把一些经典的技术手段梳理起来,结合实践融会贯通【4】
例如:封闭的作用域,订阅发布功能,策略模式,bind的应用,所以当你学习基础和细节很重要,多结合实践,多看看高质量代码,你才能更快的成长。

Git代码

代码地址

注释

1.耐心:必须要有做事持之以恒的精神
2.技巧:学习不是单纯的消耗时间,要在学习过程不断思考,形成一套有效的方法,学习效率会呈指数提升
3.前置条件: 前置条件标记技能是对于接下来的学习必须掌握的,避免阅读后续内容懵圈
4.融会贯通: 把以前学习的知识和现有的结合起来,形成自己的知识网和体系