redux-saga:运用~原理分析~理解设计目的

这是我参与8月更文挑战的第1天,活动详情查看: 8月更文挑战

前言

之前在理解redux-thunk和redux-promise?从学习redux中间件开始介绍了redux的中间件机制以及分析了redux-thunkredux-promise两个常用的中间件源码。其实对于前两种中间件,大型项目中用的最多的是redux-saga这个中间件来处理异步action,而redux-saga是一个重量级的中间件,所以单独写一篇文章来介绍以下。

运用

还是以上一篇文章的例子:即点击按钮从github请求表情图数据,在响应结束后显示表情图。这次我们用redux-saga来处理异步action

async emojis.gif

项目代码地址

先展示Redux store部分的代码,store文件目录如下:

image.png

store\action\index.js

// 异步action的creator
export const REQUEST_EMOJIS = () => ({
  type: "REQUEST_EMOJIS",
});
复制代码

store\reducer\index.js

const reducer = (state, action) => {
  switch (action.type) {
    case "SET_EMOJIS":
      return action.emojis;
    default:
      return state;
  }
};

export default reducer;
复制代码

store\sagas\index.js

import { call, put, takeEvery } from "redux-saga/effects";

const fetchEmojis = async () => {
  return fetch("https://api.github.com/emojis").then((res) => res.json());
};

function* requestEmojis() {
  const emojis = yield call(fetchEmojis);
  yield put({ type: "SET_EMOJIS", emojis });
}

function* emojiSaga() {
  yield takeEvery("REQUEST_EMOJIS", requestEmojis);
}

export default emojiSaga;
复制代码

上面的每一个生成器generator函数(上面代码的requestEmojisemojiSaga)在redux-saga的概念中被称为saga,每个saga都是用于处理异步action的函数。

store\index.js

import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import reducer from "./reducer";
import emojiSaga from "./sagas";

// 通过createSagaMiddleware生成sagaMiddleware中间件,可看出sagaMiddleware是通过工厂模式生成的
const sagaMiddleware = createSagaMiddleware();
// 传入sagaMiddleware生成store
const store = createStore(reducer, {}, applyMiddleware(sagaMiddleware));
// sagaMiddleware.run的作用是执行作为形参传入的saga
sagaMiddleware.run(emojiSaga);

export default store;
复制代码

最后我们的容器化组件的代码跟之前一样:

components\App.jsx

import React from "react";
import { connect } from "react-redux";
import { REQUEST_EMOJIS } from "../store/action/index";

const App = (props) => {
  const { emojis } = props;
  return (
    <div>
      <h2>emojis</h2>
      <button onClick={props.requestEmojis}>获取表情图</button>
      {Object.entries(emojis)
        .slice(0, 50)
        .map(([key, value]) => (
          <img src={value} key={key} alt={key} title={key} />
        ))}
    </div>
  );
};

const mapStateToProps = (state) => ({
  emojis: state,
});

const mapDispatchToProps = (dispatch) => ({
  requestEmojis: () => dispatch(REQUEST_EMOJIS()),
});

export default connect(mapStateToProps, mapDispatchToProps)(App);
复制代码

便可以呈现开头的gif中的效果。

其实,从容器化组件dispatch开始到容器化组件UI的更新,经历了下面这些过程:

image.png

我们把上面的流程图对着刚刚写的项目更加具体的细化,可得到点击按钮到页面呈现emojis时所经历的过程:

image.png

原理浅析

这里先放一张我个人学习redux-saga源码后画出来的原理图,然后基于这张图对redux-saga进行解释:

redux-saga构造原理图3.png

从图左侧的run开始讲起,首先回顾一下创建store的代码:

store\index.js

import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import reducer from "./reducer";
import emojiSaga from "./sagas";

// 通过createSagaMiddleware生成sagaMiddleware中间件,可看出sagaMiddleware是通过工厂模式生成的
const sagaMiddleware = createSagaMiddleware();
// 传入sagaMiddleware生成store
const store = createStore(reducer, {}, applyMiddleware(sagaMiddleware));
// sagaMiddleware.run的作用是执行作为形参传入的saga
sagaMiddleware.run(emojiSaga);

export default store;
复制代码

可以看到,在调用createStore创建Redux store后,会执行sagaMiddleware.run。传入run方法的形参saga。在redux-saga的概念中被称为Root Saga。如果如流程图所示需要执行多个saga(saga1saga2saga3)。则可以在Root Saga调用非阻塞APIfork,来依次运行三个saga如下所示:

export default function* rootSaga() {
    yield fork(saga1)
    yield fork(saga2)
    yield fork(saga3)
}
复制代码

重点部分来自于saga在被sagaMiddleware.run之后的执行。

首先要知道最基本的概念:generator在实例化为iterator后,iterator中每执行一次yield语句后都会停止往下执行且返回{value,done}iterator所在的作用域,直至iteratornext方法被调用后才继续执行,然后继续上述的逻辑直到iterator执行结束。

image.png

sagaMiddleware.run中,sagaMiddleware会把saga全部实例化成iterator执行,然后每一个yield出去的数据都会交由runEffect方法去执行。在saga中,我们可以用yield返回一个Promise类型的实例。runEffect会等待该promise执行完后把resolve的数据通过next的执行传进saga,此时saga就可以直接拿到promise的执行结果然后继续执行下面的语句。在saga中,yield语句就类似于等待异步执行完的法戏,让我们体验到ES7async/await的乐趣。

image.png

但我们一般不会用yield返回Promise实例(redux-saga官方说不利于写单元测试,我会在下面的章节里详细说明)。redux-saga/effects中提供了很多API让我们去生成被yield的纯对象(plain JavaScript Objects)。这些API包括forkcallputtake,这些API在传入相应的形参执行后生成的每一个纯对象在redux-saga的概念中被称为Effect,而生成这些EffectAPI被称为EffectCreatorrunEffect函数再接收到不同类型的Effect后会调用对应方法(EffectRunner)进行处理(在源码中,被take生成的Effect会调用runTakeEffect处理;被put生成的Effect会调用runPutEffect处理)。而这些EffectRunner最终会调用next函数让saga继续执行,直到程序结束或者下一个yield语句执行。

现在说一下take类型的Effect是如何被处理的,take中传入的形参pattern(这里的pattern可以是多种类型的数据如数组、字符串,redux-saga支持多种匹配方式)会连同next存放在channels里,待Container Component某个时刻dispatch一个action时,channels中所有匹配pattern的回调函数会被取出且执行,从而把执行权交回对应的saga

如果想了解更多更详细的API(即EffectCreator)可阅读effect-creators.

手写Redux-saga

这里我就不详细写内容了,直接推荐一波大佬的文章手写Redux-Saga源码,里面的mini redux-saga放在我开头的例子里也可以运行,详细可看项目代码

自己在学习Redux-saga时,直接啃源码也有很多不懂。在看了上面的文章知道原理后再看源码就顺利多了。

为什么要用redux-saga

通常在大型项目里我们会用redux-saga多于redux-thunk。一些出名的react脚手架例如dva都是选择封装redux-saga的。那redux-saga的优势表现在哪呢?优势在于能够很方便地写单元测试。下面直接展示关于上面的例子中requestEmojis的代码以及这个saga的基于jest运行的单元测试代码:

store\sagas\index.js

import { call, put, takeEvery } from "redux-saga/effects";

export const fetchEmojis = async () => {
  return fetch("https://api.github.com/emojis").then((res) => res.json());
};

export function* requestEmojis() {
  const emojis = yield call(fetchEmojis);
  yield put({ type: "SET_EMOJIS", emojis });
}
复制代码

store/__test__/saga.test.js

import { requestEmojis, fetchEmojis } from "../sagas/index";
import { put, call } from "redux-saga/effects";

describe("saga test", () => {
  const gen = requestEmojis();

  test("requestEmojis", () => {
    // 第一次gen.next(),也就是saga第一次yield,返回的value是类型为call的Effect。
    // 我们知道Effect其实是一个纯对象,所以可以用call按照saga的逻辑传入fetchEmojis再生成一个Effect与之进行深度比较。
    // 注意这里的expect语句用的是toEqual而不是toBe,toEqual是深度比较而后者是浅比较
    expect(gen.next().value).toEqual(call(fetchEmojis));
    // 第二次gen.next(),也就是saga第二次yield,返回的value是类型为put的Effect。
    // 所以也跟上面的做法一样,用put传入同步Action生成Effect与之比较
    // 注意这里的同步Action中的emojis属性的值为undefined,我们这里调用gen.next时没有传入数据,
    // 则在saga中const emojis = yield call(fetchEmojis)这里的emojis为undefined
    expect(gen.next().value).toEqual(
      put({ type: "SET_EMOJIS", emojis: undefined })
    );
  });
});
复制代码

下面是单元测试运行的效果,项目代码同样已经放在开头的项目代码地址里了。

redux-saga-test.gif

后记

之后会再写一篇关于redux-saga的文章。此处立FLAG以鼓励自己。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享