likes
comments
collection
share

react 状态管理之 redux-saga 的使用与实现原理

作者站长头像
站长
· 阅读数 49

上一篇文章# redux middleware 的使用与实现原理介绍了 redux middleware,今天这篇文章将介绍非常流行的一个用于处理Side Effectredux middleware 也就是 redux-saga

reduxreducer 是一个纯函数,也就是说同样的输入必须产生同样的输出,不能包含任何的 Side Effect 副作用, redux副作用是指在redux应用程序中,除了纯粹的状态变更外,对外部环境产生的任何操作或影响。一些常见的副作用包括:

  • 异步操作:例如发送网络请求、读取本地存储或访问数据库等。这些操作需要在某个时刻完成,并且可能会导致状态的变化
  • 访问浏览器环境:例如修改 URL 路径、访问浏览器的 cookieslocalStorage 等。这些操作与 redux 的纯函数原则不符,但是在应用中常常需要进行。
  • 访问外部资源:例如获取当前时间、获取设备的地理位置等。这些操作也属于副作用,因为它们不是通过纯函数的方式实现的。
  • 使用定时器:比如 settimeoutsetinterval
  • 生成随机数或者随机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, 接着使用 reduxapplyMiddleware 方法, 使用这个 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 了两个生成器函数 sagaIncrementWatchersagaDecrementWatcher 函数, 这两个生成器函数的内容类似, 都是监听某个 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 来触发 sagatakeactionType, 从而执行后面的逻辑!

上面代码中的 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 会响应多次 actiontake 只会执行一次, 并且 takeEvery 是非阻塞的

实际上takeEvery内部就是通过 fork 和不断地调用 take 来实现的

put

用于在 saga 中派发动作, 使用方法和 dispatch 一样, 非阻塞

例如派发一个 addaction:

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 查看。

原理及实现

前置知识

  1. function* 生成器函数, function\* 这种声明方式(function关键字后跟一个星号)会定义一个生成器函数 (generator function),它返回一个  Generator  对象。
  2. Generator 对象 生成器对象是由一个 generator function 返回的,并且它符合可迭代协议和迭代器协议。拥有三个方法:
  3. co 函数, 自动地迭代一个 Generator 对象。
  4. 判断一个对象是否是迭代器,拥有 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中一个actiondispatch 时通知监听该 action 的监听方法触发, 在 reduxtake effect 会监听 actiondispatch, 在 redux-saga 中自己实现了一套发布订阅, 这里我用比较熟悉的 events 包, 它的 APInode 中的 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, 也就是在 createSagaMiddlewarebind 的那个参数{ 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 中会调用 channelonce 函数来监听对应的 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, 并且在该 Promisethen 方法中调用 next 继续迭代 saga

takeEvery effect creator 实现

takeEvery 接收两个参数, 第一个参数是需要监听的 action.type, 第二个参数是一个生成器函数 workerSaga 用于处理监听到的 action.typetakeEvery 内部使用 forktake 来实现, 首先返回一个 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 则会将 envgetState() 执行结果及 select 传入的参数作为 selector 的参数执行,最后调用 next 方法传入得到的结果,这样就能在 yield 左侧拿到查询结果了

cancel effect creator 实现

fork 或者 takeEvery 调用之后会返回一个 task 对象,调用 cancel(task) 函数可以取消执行该任务。所以 runSaga 函数需要返回一个 task 对象,该对象拥有一个 cancel 方法,这个 cancel 方法执行后会调用 next 函数,并传入一个 CANCEL_TASK 常量,在 next 函数中判断如果传入的 valueCANCEL_TASK 那么就调用 it.return() 将这个迭代器迭代完毕,之后就不再迭代了。并且在 fork 这个 effect 中将 runSaga 的返回对象 task 作为参数传递给 next 方法,最终 yield fork() 的值就是这个 task 了。而在 runSaga 中如果 effectCANCEL 的话直接调用 task.cancel()

总结

以上就是 redux-saga 的使用与实现原理的全部内容, 篇幅有限文中还有很多 redux-sagaAPI 没有讲到, 纸上得来终觉浅,绝知此事要躬行, 还需要大家自己在项目中学习和总结。

转载自:https://juejin.cn/post/7243725129461366840
评论
请登录