这是我参与8月更文挑战的第1天,活动详情查看: 8月更文挑战
前言
之前在理解redux-thunk和redux-promise?从学习redux中间件开始介绍了redux
的中间件机制以及分析了redux-thunk
和redux-promise
两个常用的中间件源码。其实对于前两种中间件,大型项目中用的最多的是redux-saga
这个中间件来处理异步action
,而redux-saga
是一个重量级的中间件,所以单独写一篇文章来介绍以下。
运用
还是以上一篇文章的例子:即点击按钮从github请求表情图数据,在响应结束后显示表情图。这次我们用redux-saga
来处理异步action
。
先展示Redux store
部分的代码,store
文件目录如下:
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
函数(上面代码的requestEmojis
和emojiSaga
)在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的更新,经历了下面这些过程:
我们把上面的流程图对着刚刚写的项目更加具体的细化,可得到点击按钮到页面呈现emojis
时所经历的过程:
原理浅析
这里先放一张我个人学习redux-saga
源码后画出来的原理图,然后基于这张图对redux-saga
进行解释:
从图左侧的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
(saga1
、saga2
、saga3
)。则可以在Root Saga
调用非阻塞API
:fork
,来依次运行三个saga
如下所示:
export default function* rootSaga() {
yield fork(saga1)
yield fork(saga2)
yield fork(saga3)
}
复制代码
重点部分来自于saga
在被sagaMiddleware.run
之后的执行。
首先要知道最基本的概念:generator
在实例化为iterator
后,iterator
中每执行一次yield
语句后都会停止往下执行且返回{value,done}
到iterator
所在的作用域,直至iterator
的next
方法被调用后才继续执行,然后继续上述的逻辑直到iterator
执行结束。
在sagaMiddleware.run
中,sagaMiddleware
会把saga
全部实例化成iterator
执行,然后每一个yield
出去的数据都会交由runEffect
方法去执行。在saga
中,我们可以用yield
返回一个Promise
类型的实例。runEffect
会等待该promise
执行完后把resolve
的数据通过next
的执行传进saga
,此时saga
就可以直接拿到promise
的执行结果然后继续执行下面的语句。在saga
中,yield
语句就类似于等待异步执行完的法戏,让我们体验到ES7
中async/await
的乐趣。
但我们一般不会用yield
返回Promise
实例(redux-saga
官方说不利于写单元测试,我会在下面的章节里详细说明)。redux-saga/effects
中提供了很多API
让我们去生成被yield
的纯对象(plain JavaScript Objects
)。这些API
包括fork
、call
、put
、take
,这些API
在传入相应的形参执行后生成的每一个纯对象在redux-saga
的概念中被称为Effect
,而生成这些Effect
的API
被称为EffectCreator
。runEffect
函数再接收到不同类型的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
的文章。此处立FLAG以鼓励自己。