1. redux-saga #

1.1 介绍 #

redux-saga 是一个用于管理应用程序副作用(例如数据获取、访问浏览器缓存、设备API的调用等)的库。它的主要功能是使 redux 应用程序的副作用更容易管理、执行、测试和调试。

以下是 redux-saga 的主要概念和功能:

  1. Generators: redux-saga 使用 ES6 的 Generator 函数来执行 sagas(这是一个核心概念)。Generators 允许函数暂停并恢复执行,这对于管理异步代码非常有用。

  2. Effects: Effects 是一个 JavaScript 对象,用于描述某些控制流指令。例如,call 是一个 effect,用于调用异步函数。另一个常用的 effect 是 put,用于分发一个 action。

  3. Watcher Sagas: 这些 sagas "监听"特定的 action,并根据这些 action 触发其他 sagas。例如,您可能有一个 watcher saga,当检测到 FETCH_DATA_REQUEST action 时,它会启动一个 saga 来获取数据。

  4. Worker Sagas: 当 watcher sagas 捕获到一个特定的 action 时,worker sagas 会被调用执行实际的异步操作。

  5. Blocking vs Non-blocking Calls: 使用 yield call(fn, ...args) 是阻塞调用,saga 会等待函数执行完毕。而使用 yield fork(fn, ...args) 则是非阻塞调用,saga 会继续执行下一个指令。

  6. Error Handling: 因为 sagas 使用 JavaScript 生成器,所以你可以使用普通的 try/catch 语句来处理错误。

  7. Cancellation: Sagas 可以被取消。例如,如果用户在数据加载时切换了页面,您可能想取消正在进行的数据获取操作。

  8. Parallel Execution: 使用 yield all([saga1(), saga2(), ...]) 可以并行执行多个 sagas。

总之,redux-saga 提供了一种优雅的方式来处理复杂的副作用管理。与 redux 的 thunkpromise 中间件相比,它提供了更多的功能和更大的灵活性。

1.2 安装 #

npm install redux redux-saga react-redux --save

2. take&put #

take

putredux-saga 中的一个 Effect 创建器。在 redux-saga 的上下文中,Effect 可以被认为是一个描述了某种副作用(如异步操作、事件订阅等)的对象,但它自身并不执行这些副作用。这允许我们编写纯函数式的、可测试的代码。

put 用于创建一个 Effect,描述了向 Redux store 发出一个 action 的过程。这与 dispatch 函数在纯 Redux 中的角色相似。

使用方式

在一个 redux-saga 的 generator 函数中,可以如下使用 put

import { put } from 'redux-saga/effects';

function* someSaga() {
  // ... some logic
  yield put({ type: 'SOME_ACTION', payload: 'some data' });
  // ... more logic
}

在上述例子中,我们在 saga 中使用了 put Effect 来发出一个 SOME_ACTION 的 action。当 saga 到达这一行时,这个 action 会被发往 Redux store,就好像我们在传统的 Redux 中调用 dispatch({ type: 'SOME_ACTION', payload: 'some data' }) 一样。

工作原理

当 saga interpreter(saga 的运行时)碰到一个 put Effect 时,它会知道要将对应的 action 对象发送到 Redux store。这一行为是由 middleware(saga middleware)完成的,因此你不需要直接与 Redux store 互动。

注意点

总的来说,put Effect 提供了一种纯函数式、可测试的方式来描述向 Redux store 发出 action 的过程,从而使得我们能够在 sagas 中处理复杂的逻辑和异步操作。

put

redux-saga 是一个用于管理应用程序 Side Effects(例如:异步获取数据、访问浏览器缓存等)的库。它的目标是使副作用管理更加简单和高效,同时使测试变得容易。

redux-saga 中,put 是一个非常重要的 effect,用于触发 action。与 Redux 中的 dispatch 方法类似,但它的工作方式有些不同。

put的基本介绍

  1. 定义put 是一个 effect 创建器,它返回一个纯对象描述应该发生的 Side Effect。具体来说,它会告诉 redux-saga middleware 需要分发一个 action 到 Redux store。

  2. 返回值put 返回一个 Effect 描述对象,该对象将被 redux-saga middleware 解释并执行相应的副作用。

  3. 使用方式

import { put } from 'redux-saga/effects';

function* someSaga() {
  // 使用 put 发出 action
  yield put({ type: 'ACTION_TYPE', payload: {} });
}

为什么使用 put 而不是 dispatch

虽然在功能上看起来类似于 dispatch,但 putdispatch 有一些关键的区别:

  1. 声明性put 创建的是一个 Effect 描述对象,而不是直接触发 action。这允许我们编写纯净、可测试的 generator 函数,因为它们不直接产生副作用。

  2. 顺序执行:当你在 saga 中 yield 一个 put effect,saga 会等待这个 action 完全处理完成(即所有相应的 reducers 和 sagas 完全处理完成后)再继续执行后面的代码。

  3. 测试:由于 put 只是返回一个描述对象,所以在测试中,你可以轻松地检查这些描述对象,而不必担心它们的执行情况。

结论

redux-saga 中,put 提供了一种声明性的方式来分发 actions。它使我们能够在一个统一的上下文中处理异步流程和 Redux actions,而不必担心异步操作和状态更新之间的时序问题。

2.1 index.js #

src/index.js

// 引入 React 核心库
import React from 'react'
// 引入 ReactDOM 客户端渲染库
import ReactDOM from 'react-dom/client';
// 引入 Counter 组件
import Counter from './components/Counter';
// 引入 react-redux 的 Provider 组件
import { Provider } from 'react-redux';
// 引入 Redux store
import store from './store';
// 使用 ReactDOM 的新的 Concurrent 模式渲染方法,将 App 组件渲染到页面的 #root 元素上
ReactDOM.createRoot(document.querySelector('#root')).render(
  // 使用 Provider 组件将 Redux store 传递给应用的所有子组件
  <Provider store={store}>
    <Counter />
  </Provider>
)

2.2 rootSaga.js #

src\store\rootSaga.js

// 从 redux-saga/effects 中引入 put 和 take 方法
import {put,take} from 'redux-saga/effects';
// 引入 action 类型常量
import * as actionTypes from './action-types';
// 定义一个延迟函数,返回一个延迟指定毫秒数的 Promise
function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}
// 定义一个工作 Saga,用于执行异步任务
function * workerSaga(){
  // 使用延迟函数等待1秒
  yield delay(1000);
  // 执行 put effect,派发一个 ADD 的 action
  yield put({type:actionTypes.ADD});
}
// 定义一个观察者 Saga,用于监听特定的 action,并触发对应的工作 Saga
function * watcherSaga(){
  // 使用 take effect 监听 ASYNC_ADD 类型的 action
  yield take(actionTypes.ASYNC_ADD);
  // 当上面的 action 被捕获时,执行 workerSaga
  yield workerSaga();
}
// 导出一个根 Saga,用于在 store 中运行
export default function* rootSaga() {
  // 执行观察者 Saga
  yield watcherSaga();
}

2.3 Counter.js #

src/components/Counter.js

// 从 React 中引入 React
import React from 'react';
// 引入 action 类型常量
import * as actionTypes from '../store/action-types';
// 从 react-redux 中引入 useSelector 和 useDispatch 钩子函数
import { useSelector, useDispatch } from 'react-redux';
// 定义 Counter 组件
function Counter() {
    // 使用 useSelector 钩子从 Redux store 中获取 number 值
    const number = useSelector(state => state.number);
    // 使用 useDispatch 钩子获取 dispatch 函数,用于派发 action
    const dispatch = useDispatch();
    // 返回组件的 JSX 结构
    return (
        <div>
            // 渲染 number 值
            <p>{number}</p>
            // 当点击按钮时,派发一个 ASYNC_ADD 的 action
            <button onClick={() => dispatch({ type: actionTypes.ASYNC_ADD })}>+</button>
        </div>
    )
}
// 导出 Counter 组件
export default Counter;

2.4 index.js #

src/store/index.js

// 从redux中引入createStore和applyMiddleware方法,注意createStore使用的是老版本的命名
import {legacy_createStore as createStore, applyMiddleware} from 'redux';
// 引入reducer
import reducer from './reducer';
// 引入redux-saga中的createSagaMiddleware方法
import createSagaMiddleware from 'redux-saga';
// 引入rootSaga
import rootSaga from './rootSaga';
// 创建saga中间件
let sagaMiddleware = createSagaMiddleware();
// 创建并应用saga中间件到store
let store = applyMiddleware(sagaMiddleware)(createStore)(reducer);
// 运行rootSaga
sagaMiddleware.run(rootSaga);
// 将store挂载到window对象上,方便调试
window.store = store;
// 导出store
export default store;

2.5 action-types.js #

src/store/action-types.js

export const ASYNC_ADD='ASYNC_ADD';
export const ADD='ADD';

2.6 reducer.js #

src/store/reducer.js

import * as types from './action-types';
export default function reducer(state={number:0},action) {
    switch(action.type){
        case types.ADD:
            return {number: state.number+1};
        default:
            return state;
    }
}

3. 支持fork #

fork

forkredux-saga 提供的一个 Effect 创建器。与其他的 Effects(如 call, put, take 等)相似,fork 也用于描述在 Redux Saga 任务中需要执行的副作用。不过,与其他 effect 有所不同的是,fork 用于非阻塞调用。

让我们深入了解一下 fork 的特性和如何使用它:

非阻塞调用

使用方式

import { fork } from 'redux-saga/effects';

function* backgroundTask() {
  // 这里的代码是后台任务的代码
}

function* mySaga() {
  yield fork(backgroundTask);
  // 当我们达到这一行时,backgroundTask 已经在后台启动,并且 mySaga 不会等待它完成。
}

3. 错误处理

4. 取消任务

import { fork, cancel, take } from 'redux-saga/effects';

function* backgroundTask() {
  // 这里的代码是后台任务的代码
}

function* mySaga() {
  const task = yield fork(backgroundTask);

  // 等待一个特定的 action
  yield take('STOP_BACKGROUND_TASK');

  // 然后取消任务
  yield cancel(task);
}

结论

redux-saga 中,fork 提供了一种方法来启动并行的非阻塞任务。这在处理后台任务或其他与 UI 更新无关的任务时非常有用。而与 fork 一起使用的 cancel effect 则允许我们在需要时取消这些任务。

3.1 rootSaga.js #

+import {put,take,fork} from 'redux-saga/effects';
import * as actionTypes from './action-types';
function delay(ms) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}
function * workerSaga(){
  yield delay(1000);
  yield put({type:actionTypes.ADD});
}
function * watcherSaga(){
  yield take(actionTypes.ASYNC_ADD);
+ yield fork(workerSaga);
}

export default function* rootSaga() {
  yield watcherSaga();
}

4. 支持takeEvery #

takeEvery

takeEveryredux-saga 中的一个高级 Effect 创建器,用于监听传入的特定类型的 action,并为每个接收到的 action 执行给定的 saga。

让我们详细了解 takeEvery 的工作原理和如何使用它:

1. 基本工作原理

2. 使用方式

import { takeEvery } from 'redux-saga/effects';

function* handleSomeAction(action) {
  // 处理 action 的逻辑
}

function* mySaga() {
  yield takeEvery('SOME_ACTION_TYPE', handleSomeAction);
}

在上面的例子中,每当一个类型为 'SOME_ACTION_TYPE' 的 action 被派发时,handleSomeAction saga 将被启动,并接收该 action 作为其参数。

3. 非阻塞

4. 实现细节

如果你想知道 takeEvery 的背后实现,实际上它是使用 takefork 实现的。以下是一个简化的版本:

function* takeEvery(actionType, saga) {
  while (true) {
    const action = yield take(actionType);
    yield fork(saga, action);
  }
}

5. 注意事项

结论

takeEveryredux-saga 提供的一个非常有用的工具,用于响应并处理特定类型的 action。在许多常见的应用场景中,它都很有用,但在使用时需要考虑其非阻塞性质和可能产生的并行任务。

4.1 rootSaga.js #

+import {put,takeEvery} from 'redux-saga/effects';
import * as actionTypes from './action-types';
export function* add() {
  yield put({ type: actionTypes.ADD });
}
export default function* rootSaga() {
+  yield takeEvery(actionTypes.ASYNC_ADD,add);
}

5. 支持 call #

call

callredux-saga 的一个 Effect 创建器,它用于调用函数或方法,并返回一个纯对象(Effect 描述对象),而不是直接执行它。这使得我们的 sagas 更容易测试,因为我们可以检查这些 Effect 描述对象,而不必实际执行任何副作用。

下面详细了解 call

1. 基本工作原理

2. 使用方式

调用普通函数

import { call } from 'redux-saga/effects';

function fetchData() {
  return fetch('/api/data').then(res => res.json());
}

function* mySaga() {
  const data = yield call(fetchData);
  // 一旦 fetchData 完成,以下代码将会执行
  console.log(data);
}

调用对象方法:

const api = {
  fetchData() {
    return fetch('/api/data').then(res => res.json());
  }
};

function* mySaga() {
  const data = yield call([api, api.fetchData]);
  console.log(data);
}

传递参数:

function fetchItem(id) {
  return fetch(`/api/data/${id}`).then(res => res.json());
}

function* mySaga() {
  const id = 1;
  const item = yield call(fetchItem, id);
  console.log(item);
}

3. 为什么使用 call

使用 call 的主要好处是它使得你的 saga 更容易进行单元测试。由于 call 只是返回一个 Effect 描述对象而不是直接执行函数,我们可以轻松地断言该对象,而不必实际运行任何异步逻辑。

4. 注意事项

结论

callredux-saga 中的一个核心 Effect,它允许我们在 saga 中进行阻塞式的函数调用。这不仅使我们的异步流程更容易理解,而且使我们的 sagas 更容易测试。

5.1 rootSaga.js #

src\store\rootSaga.js

// 从redux-saga中引入put,takeEvery和call效果
import {put, takeEvery, call} from 'redux-saga/effects';
// 引入action类型常量
import * as actionTypes from './action-types';
// 定义一个延迟函数
const delay = ms => new Promise((resolve) => {
  // 使用setTimeout进行延迟
  setTimeout(() => {
      resolve();
  }, ms);
});
// 定义一个saga生成器函数,用于处理异步加法
export function* add() {
  // 使用call效果来调用delay函数实现延迟
  yield call(delay, 1000);
  // 使用put效果派发ADD action
  yield put({ type: actionTypes.ADD });
}
// 定义rootSaga生成器函数
export default function* rootSaga() {
  // 使用takeEvery监听每一个ASYNC_ADD action,并触发add saga处理
  yield takeEvery(actionTypes.ASYNC_ADD, add);
}

6. 支持 cps #

cps

redux-saga 中,cps 是一个 Effect 创建器,用于处理 Node 风格的回调函数(即采用 (error, result) => ... 的回调函数)。在 Node.js 生态中,许多旧的库和某些核心模块使用这种风格的回调。

当我们需要在 saga 中调用这类函数时,cps 变得很有用。

基本工作原理

使用方式

考虑以下一个简单的 Node 风格的读文件函数:

const fs = require('fs');

function readFile(path, cb) {
  fs.readFile(path, 'utf-8', cb);
}

在 saga 中,我们可以使用 cps 如下:

import { cps } from 'redux-saga/effects';

function* readSaga() {
  try {
    const content = yield cps(readFile, '/path/to/file.txt');
    console.log(content);
  } catch (err) {
    console.error('File read failed:', err);
  }
}

为什么使用 cps

注意事项

结论

cpsredux-saga 中处理 Node 风格回调函数的特定 Effect。当与此类回调 API 进行交互时,它为 saga 提供了一个简洁、一致的方式。

6.1 rootSaga.js #

src\store\rootSaga.js

// 从redux-saga中引入put,takeEvery,call和cps效果
import {put, takeEvery, call, cps} from 'redux-saga/effects';
// 引入action类型常量
import * as actionTypes from './action-types';
// 定义一个带回调的延迟函数
const delay = (ms, callback) => {
  // 使用setTimeout进行延迟,并在完成后调用回调函数
  setTimeout(() => {
      callback(null, 'ok');
  }, ms);
}
// 定义一个saga生成器函数,用于处理异步加法
export function* add() {
  // 使用cps效果调用带回调的delay函数并获取结果
  let data = yield cps(delay, 1000);
  // 打印cps返回的数据
  console.log(data);
  // 使用put效果派发ADD action
  yield put({ type: actionTypes.ADD });
}
// 定义rootSaga生成器函数
export default function* rootSaga() {
  // 使用takeEvery监听每一个ASYNC_ADD action,并触发add saga处理
  yield takeEvery(actionTypes.ASYNC_ADD, add);
}

7. 支持all #

all

allredux-saga 的一个 Effect 创建器。它的主要作用是并行执行多个 Effects,并等待它们全部完成。这是一个非常有用的工具,尤其是当你想同时启动多个任务,并等待所有任务都完成时。

以下是 all 的主要特点和使用方法:

1. 基本工作原理

2. 使用方式

假设我们有两个可以并行执行的异步任务,我们可以使用 all 来同时执行它们:

import { all, call } from 'redux-saga/effects';

function* fetchUsers() {
  const users = yield call(fetchAPI, '/users');
  // ... 处理 users
}

function* fetchProducts() {
  const products = yield call(fetchAPI, '/products');
  // ... 处理 products
}

function* mySaga() {
  const [users, products] = yield all([fetchUsers(), fetchProducts()]);
  // 这里,users 和 products 都已经准备好了
}

在上面的示例中,fetchUsersfetchProducts 任务会并行执行。只有当这两个任务都完成时,mySaga 才会继续执行。

3. 错误处理

如果传递给 all 的任何 Effect 失败(例如,一个 Promise 被拒绝),则 all 也会立即失败,并拒绝首个失败的 Effect 的错误。此时,其他尚未完成的 Effects 会继续执行,但它们的结果将被忽略。

4. 为什么使用 all

结论

allredux-saga 提供的一个强大的 Effect 创建器,用于并行执行多个 Effects 并等待它们全部完成。当需要并行处理多个任务时,它是非常有用的。

7.2 rootSaga.js #

src\store\rootSaga.js

// 从redux-saga中引入所需的效果
import { put, take , call, cps, all } from 'redux-saga/effects';
// 引入action类型常量
import * as actionTypes from './action-types';
// 定义一个saga生成器函数,用于处理一次异步加法
export function* add1() {
  // 循环一次
  for (let i = 0; i < 1; i++) {
    // 等待一个ASYNC_ADD action
    yield take(actionTypes.ASYNC_ADD);
    // 派发一个ADD action
    yield put({ type: actionTypes.ADD });
  }
  // 打印完成的消息
  console.log('add1 done ');
  // 返回结果
  return 'add1Result';
}
// 定义另一个saga生成器函数,用于处理两次异步加法
export function* add2() {
  // 循环两次
  for (let i = 0; i < 2; i++) {
    // 等待一个ASYNC_ADD action
    yield take(actionTypes.ASYNC_ADD);
    // 派发一个ADD action
    yield put({ type: actionTypes.ADD });
  }
  // 打印完成的消息
  console.log('add2 done ');
  // 返回结果
  return 'add2Result';
}
// 定义rootSaga生成器函数
export default function* rootSaga() {
  // 使用all效果并行执行add1和add2,并等待它们都完成
  let result = yield all([add1(), add2()]);
  // 打印完成消息和结果
  console.log('done', result);
}

8. 取消任务 #

cancel

cancelredux-saga 的一个 Effect 创建器,它用于取消正在运行的任务(通常是由 fork, spawn, 或 race 所创建的任务)。当任务被取消时,如果该任务正在执行一个阻塞调用(例如 calltake),那么该调用会被中断并抛出一个 SagaCancellationException

以下是关于 cancel 的详细说明:

1. 基本工作原理

2. 使用方式

假设有一个 backgroundTask 函数,我们希望在特定的 action 被触发时取消这个任务:

import { fork, take, cancel } from 'redux-saga/effects';

function* backgroundTask() {
  // ... 执行一些操作
}

function* watchStartBackgroundTask() {
  const bgTask = yield fork(backgroundTask);
  yield take('STOP_BACKGROUND_TASK');
  yield cancel(bgTask);
}

在上述代码中,当 watchStartBackgroundTask 被触发时,backgroundTask 会作为一个后台任务启动。当 STOP_BACKGROUND_TASK action 被触发时,我们使用 cancel Effect 来取消 backgroundTask

3. 为什么使用 cancel

function* cancellableTask() {
  try {
    // 执行任务
  } catch (e) {
    if (e instanceof SagaCancellationException) {
      // 处理取消逻辑,例如资源清理
    } else {
      throw e;
    }
  }
}

4. 注意事项

结论

cancelredux-saga 的一个重要 Effect,它为我们提供了在特定条件下终止正在运行的任务的能力。当结合 fork, spawn, 和 race 使用时,cancel 可以帮助我们有效地控制和管理并发操作。

8.1 rootSaga.js #

src\store\rootSaga.js

// 从redux-saga中引入所需的效果
import { put, take, cancel, fork, delay } from 'redux-saga/effects';
// 引入action类型常量
import * as actionTypes from './action-types';
// 定义一个saga生成器函数,持续地每隔1秒发起ADD action
export function* add() {
  // 无限循环
  while (true) {
    // 延迟1秒
    yield delay(1000);
    // 派发一个ADD action
    yield put({
      type: actionTypes.ADD
    });
  }
}
// 定义监视器saga生成器函数,它负责fork出上面的add saga并在需要时取消它
export function* addWatcher() {
  // 使用fork开启一个新的add saga任务
  const task = yield fork(add);
  // 打印fork返回的任务对象
  console.log(task);
  // 等待一个STOP_ADD action
  yield take(actionTypes.STOP_ADD);
  // 取消add saga任务
  yield cancel(task);
}
// 定义请求saga生成器函数,用于处理网络请求
function* request(action) {
  // 获取请求的url
  let url = action.payload;
  // 创建一个网络请求的promise
  let promise = fetch(url).then(res => res.json());
  // 等待promise完成并获取响应数据
  let res = yield promise;
  // 打印响应数据
  console.log(res);
}
// 定义监视器saga生成器函数,它负责fork出上面的request saga并在需要时取消它
function* requestWatcher() {
  // 等待一个REQUEST action
  const requestAction = yield take(actionTypes.REQUEST);
  // 使用fork开启一个新的request saga任务并传入action
  const requestTask = yield fork(request, requestAction);
  // 等待一个STOP_REQUEST action
  yield take(actionTypes.STOP_REQUEST);
  // 取消request saga任务
  yield cancel(requestTask);
}
// 定义rootSaga生成器函数
export default function* rootSaga() {
  // 执行addWatcher saga
  yield addWatcher();
  // 执行requestWatcher saga
  yield requestWatcher();
}

8.2 action-types.js #

src\store\action-types.js

export const ASYNC_ADD='ASYNC_ADD';
export const ADD='ADD';

+export const STOP_ADD='STOP_ADD';

+export const REQUEST = 'REQUEST';
+export const STOP_REQUEST = 'STOP_REQUEST';

8.3 Counter.js #

src\components\Counter.js

import React from 'react';
import * as actionTypes from '../store/action-types';
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
    const number = useSelector(state => state.number);
    const dispatch = useDispatch();
    return (
        <div>
            <p>{number}</p>
+           <button onClick={() => dispatch({ type: actionTypes.ASYNC_ADD })}>+</button>
+           <button onClick={() => dispatch({ type: actionTypes.STOP_ADD })}>stop</button>
+           <button onClick={() => dispatch({ type: actionTypes.REQUEST, payload: '/users.json' })}>request</button>
+           <button onClick={() => dispatch({ type: actionTypes.STOP_REQUEST })}>stopRequest</button>
        </div>
    )
}
export default Counter;