https://github.com/redux-saga/redux-saga
英文官网:https://redux-saga.js.org/
中文翻译:https://redux-saga-in-chinese.js.org/

简述

  • redux-saga 是一个redux的中间件,而中间件的作用是为 redux 提供额外的功能。
  • 在 reducers 中的所有操作都是同步的并且是纯粹的,即 reducer 都是纯函数,纯函数是指一个函数的返回结果只依赖于它的参数,并且在执行过程中不会对外部产生副作用。
  • 但是在实际的应用开发中,我们希望做一个异步的(如Ajax请求)且不纯粹的操作(如改变外部的状态),这些在函数式编程范式中被称为“副作用”。

    redux-saga 就是用来处理上述副作用(异步任务)的一个中间件。它是一个接收事件,并可能触发新事件的过程管理者,为你的应用管理复杂的流程。

redux-saga 工作原理

  • sagas采用Generator函数来 yield Effects(包含指令的文本对象)。
  • Generator 函数的作用是可以暂停执行,再次执行的时候从上次暂停的地方继续执行。
  • Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。
  • 你可以通过使用 effects API 如:fork、call、take、put、cancel等来创建 Effect。

redux-saga 分类

  • worker saga 做左右的工作,如调用API,进行异步请求,获取异步封装的结果。
  • watcher saga 监听被dispatch的actions,当接受到actions或者知道其被触发时,调用worker执行任务。
  • root saga 立即启动saga的唯一入口。

image.png

Generator 回顾

https://www.yuque.com/zhuchaoyang/wrif6k/ozcfr0

  1. // 生成器
  2. function* gen(){
  3. console.log('start');
  4. let a = yield 1;
  5. console.log('a=>', a);
  6. let b = 10 * (yield 2);
  7. console.log('b=>', b);
  8. let c = yield 3;
  9. console.log('c=>', c);
  10. }
  11. // 生成器执行返回一个迭代器
  12. let it = gen();
  13. console.log(it.next());
  14. // start
  15. // {value: 1, done: false} value等于yield后面的那个表示式的值
  16. console.log(it.next());
  17. // a=> undefined
  18. // {value: 2, done: false}
  19. console.log(it.next(4)); //参数4等于上一个yield表达式的值,即 b = 10 * 4
  20. // b=> 40
  21. // {value: 3, done: false}
  22. console.log(it.next());
  23. // {value: undefined, done: true}

take、put

  • take 监听某个动作的发生。如果有人派发了这个动作,当前saga就继续执行;如果没有,就阻塞在这里。
  • put 派发一个真正的动作。

rwe.gif

src/index.js

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. import {Provider} from 'react-redux';
  4. import store from './store';
  5. import Counter from './components/Counter';
  6. function App(){
  7. return (
  8. <Provider store={store}>
  9. <Counter />
  10. </Provider>
  11. )
  12. }
  13. ReactDOM.render(<App />, document.querySelector('#root'));

src/store/index.js

  1. import {createStore, applyMiddleware} from 'redux';
  2. import createSagaMiddleware from 'redux-saga';
  3. import reducers from './reducers';
  4. import rootSaga from './sagas';
  5. let sagaMiddleware = createSagaMiddleware(); //创建saga中间件
  6. let store = applyMiddleware(sagaMiddleware)(createStore)(reducers);
  7. sagaMiddleware.run(rootSaga); //启动rootSaga
  8. export default store;

src/store/action-types.js

  1. export const ASYNC_ADD = 'ASYNC_ADD'; //异步加1,发送给监听saga
  2. export const ADD = 'ADD'; //同步加1,发送给worker saga
  3. export const ASYNC_MINUS = 'ASYNC_MINUS'; //异步减1
  4. export const MINUS = 'MINUS'; //同步减1

src/store/sagas

src/store/sagas/index.js
  1. import {take, put} from 'redux-saga/effects';
  2. import * as actionTypes from '../action-types';
  3. // 根saga
  4. export default function* rootSaga(){
  5. console.log('rootSaga开始执行');
  6. for (let i=0; i<3; i++){ //只会执行3次,3次之后,再点就没有效果了
  7. // for (;;){ //如果不想这样,可以写一个死循环,每次点都是有效果的。
  8. // 在此监听某个动作的发生,
  9. // 如果有人派发这个动作,当前saga就继续执行;如果没有,就暂停阻塞在这里。
  10. let aa = yield take(actionTypes.ASYNC_ADD);
  11. console.log('此处模拟一个延迟 1 秒', aa); //{type: "ASYNC_ADD"}
  12. // 派发一个真正的动作,即 store.dispatch({type: actionTypes.ADD})
  13. let bb = yield put({type: actionTypes.ADD});
  14. console.log(bb); //{type: "ADD", @@redux-saga/SAGA_ACTION: true}
  15. }
  16. console.log('rootSaga结束执行');
  17. }

src/store/actions

src/store/actions/counter.js
  1. import * as actionTypes from '../action-types';
  2. const actions = {
  3. add(){
  4. return {type: actionTypes.ADD};
  5. },
  6. asyncAdd(){
  7. return {type: actionTypes.ASYNC_ADD, payload: {step: 2}};
  8. },
  9. minus(){
  10. return {type: actionTypes.MINUS};
  11. },
  12. asyncMinus(){
  13. return {type: actionTypes.ASYNC_MINUS};
  14. },
  15. }
  16. export default actions;

src/store/reducers

src/store/reducers/index.js
  1. import * as actionTypes from '../action-types';
  2. function reducer(state={number:0}, action){
  3. switch(action.type){
  4. case actionTypes.ADD:
  5. return {number: state.number+1};
  6. case actionTypes.MINUS:
  7. return {number: state.number-1};
  8. default:
  9. return state;
  10. }
  11. }
  12. export default reducer;

src/components/Counter.js

  1. import React from 'react';
  2. import {connect} from 'react-redux';
  3. import actions from '../store/actions/counter.js';
  4. class Counter extends React.Component {
  5. render(){
  6. let ps = this.props;
  7. return (
  8. <div>
  9. <p>{ps.number}</p>
  10. <div>
  11. <button onClick={ps.add}>同步+</button>
  12. <button onClick={ps.asyncAdd}>异步+</button>
  13. </div>
  14. <div>
  15. <button onClick={ps.minus}>同步-</button>
  16. <button onClick={ps.asyncMinus}>异步-</button>
  17. </div>
  18. </div>
  19. )
  20. }
  21. }
  22. export default connect(state => state, actions)(Counter)

产出 iterator

src/store/sagas/index.js

  1. import {take, put} from 'redux-saga/effects';
  2. import * as actionTypes from '../action-types';
  3. + // work saga
  4. + function* add(){
  5. + yield put({type: actionTypes.ADD});
  6. + }
  7. // 根saga
  8. export default function* rootSaga(){
  9. console.log('rootSaga开始执行');
  10. for (let i=0; i<3; i++){
  11. yield take(actionTypes.ASYNC_ADD);
  12. - // yield put({type: actionTypes.ADD});
  13. + yield add(); //产出一个迭代器
  14. }
  15. console.log('rootSaga结束执行');
  16. }

takeEvery

  • 一个task就像是一个在后台运行的进程,在基于redux-saga的应用程序中,可以同时运行多个task
  • 通过fork函数来创建task

    拆分前 src/store/sagas/index.js

    rwe.gif ```javascript import {take, put, takeEvery} from ‘redux-saga/effects’; import * as actionTypes from ‘../action-types’;

// work saga function* addWorker(){ yield put({type: actionTypes.ADD}); }

// 根saga export default function* rootSaga(){ console.log(‘rootSaga开始执行’); // 监听每一次的ASYNC_ADD动作,等到后执行add, takeEvery并不会阻塞当前的saga。 let aa = yield takeEvery(actionTypes.ASYNC_ADD, addWorker); console.log(‘rootSaga结束执行’, aa); //{@@redux-saga/TASK: true, …} }

  1. <a name="gllq5"></a>
  2. ## 拆分后 src/store/sagas/index.js
  3. ```javascript
  4. import addListener from './add';
  5. // 根saga
  6. export default function* rootSaga(){
  7. yield addListener();
  8. }

拆分后 src/store/sagas/add.js

  1. import {put, takeEvery} from '../../redux-saga/effects';
  2. import * as actionTypes from '../action-types';
  3. // work saga
  4. function* addWorker(action){
  5. console.log('action=>', action); //action=> {type: "ASYNC_ADD", payload: {step: 2}}
  6. yield put({type: actionTypes.ADD});
  7. }
  8. // 监听saga
  9. function* addListener(){
  10. yield takeEvery(actionTypes.ASYNC_ADD, addWorker);
  11. }
  12. export default addListener;

支持Promise

src/store/sagas/delay.js

  1. // 模拟延迟
  2. export const delay = (ms=1000, val=true) => new Promise((resolve, reject) => {
  3. setTimeout(resolve, ms, val);
  4. });
  5. // 模拟异步返回数据
  6. export const delayMock = (data, ms) => delay(ms, {
  7. result: 1,
  8. data,
  9. })

src/store/sagas/add.js

  1. import {put, takeEvery} from 'redux-saga/effects';
  2. import * as actionTypes from '../action-types';
  3. + import {delayMock} from './delay';
  4. // work saga
  5. function* addWorker(action){
  6. + const data = yield delayMock({id: 1, name: 'jack'});
  7. + console.log('异步返回数据=>', data); //异步返回数据=> {result: 1, data: {id: 1, name: "jack"}}
  8. yield put({type: actionTypes.ADD});
  9. }
  10. // 监听saga
  11. function* addListener(){
  12. yield takeEvery(actionTypes.ASYNC_ADD, addWorker);
  13. }
  14. export default addListener;

call 处理一个 返回值为Promise的函数

  • yield call(fn, arg1, arg2, ...)

    src/store/sagas/add.js

    ```javascript
  • import {put, takeEvery, call} from ‘redux-saga/effects’; import * as actionTypes from ‘../action-types’; import {delayMock} from ‘./delay’;

// work saga function* addWorker(action){

  • const data = yield call(delayMock, {id: 1, name: ‘jack’}); console.log(‘异步返回数据=>’, data); //异步返回数据=> {result: 1, data: {id: 1, name: “jack”}} yield put({type: actionTypes.ADD}); }

// 监听saga function* addListener(){ yield takeEvery(actionTypes.ASYNC_ADD, addWorker); }

export default addListener;

  1. <a name="UIyvv"></a>
  2. # cps 处理一个 回调形式的函数
  3. - `yield cps(fn, arg1, arg2, ...)`
  4. <a name="LI1p7"></a>
  5. ## src/store/sagas/add.js
  6. ```javascript
  7. import {put, takeEvery, call, cps} from 'redux-saga/effects';
  8. import * as actionTypes from '../action-types';
  9. import {delayMock} from './delay';
  10. // 模拟回调函数形式
  11. const delayCallback = (ms, callback) => {
  12. setTimeout(() => {
  13. let number = Math.random();
  14. if (number > 0.5){
  15. callback(null, number);
  16. } else {
  17. callback('太小了', number);
  18. }
  19. }, ms)
  20. }
  21. // work saga
  22. function* addWorker(action){
  23. //const data = yield call(delayMock, {id: 1, name: 'jack'});
  24. const data = yield cps(delayCallback, 1000)
  25. console.log('异步返回数据=>', data); //异步返回数据=> 0.536148468128234
  26. yield put({type: actionTypes.ADD});
  27. }
  28. // 监听saga
  29. function* addListener(){
  30. yield takeEvery(actionTypes.ASYNC_ADD, addWorker);
  31. }
  32. export default addListener;

all

如果有多个监听saga,用 all 实现类似于Promise.all的效果。
rwe.gif

src/store/sagas/index.js

  1. import {all} from '../../redux-saga/effects';
  2. import addListener from './add';
  3. import minusListener from './minus';
  4. // 根saga
  5. export default function* rootSaga(){
  6. // yield addListener();
  7. // yield minusListener();
  8. // 如果有多个监听saga,用 all 实现类似于Promise.all的效果。
  9. // 上图示例如下:必须等2个监听saga都完成了(3个+,2个- 动作),根rootSagac才继续执行。
  10. // 如果有任意监听saga未完成,就会先等着。
  11. let result = yield all([
  12. addListener(),
  13. minusListener(),
  14. ]);
  15. console.log('result=>', result);
  16. }

src/store/sagas/add.js

  1. import {put, take, takeEvery, call} from '../../redux-saga/effects';
  2. import * as actionTypes from '../action-types';
  3. import {delayMock} from './delay';
  4. // work saga
  5. function* addWorker(action){
  6. const data = yield call(delayMock, {id: 1, name: 'jack'});
  7. yield put({type: actionTypes.ADD});
  8. }
  9. // 监听saga
  10. function* addListener(){
  11. // yield takeEvery(actionTypes.ASYNC_ADD, addWorker);
  12. for (let i=0; i<3; i++){
  13. yield take(actionTypes.ASYNC_ADD);
  14. yield put({type: actionTypes.ADD});
  15. }
  16. return 'add3';
  17. }
  18. export default addListener;

src/store/sagas/minus.js

  1. import {put, take, takeEvery, call} from '../../redux-saga/effects';
  2. import * as actionTypes from '../action-types';
  3. import {delayMock} from './delay';
  4. // work saga
  5. function* minusWorker(action){
  6. const data = yield call(delayMock);
  7. yield put({type: actionTypes.MINUS});
  8. }
  9. // 监听saga
  10. function* minusListener(){
  11. // yield takeEvery(actionTypes.ASYNC_MINUS, minusWorker);
  12. for (let i=0; i<2; i++){
  13. yield take(actionTypes.ASYNC_MINUS);
  14. yield put({type: actionTypes.MINUS});
  15. }
  16. return 'minus2';
  17. }
  18. export default minusListener;

cancel 取消saga任务

rwe.gif

src/store/sagas/index.js

  1. import {all, fork, put, take, delay, cancel} from '../../redux-saga/effects';
  2. import * as actionTypes from '../action-types';
  3. // 每隔1秒加1
  4. function* add(){
  5. while(true){
  6. yield delay(1000);
  7. yield put({type: actionTypes.ADD});
  8. }
  9. }
  10. function* addListener(){
  11. // 开启一个新的子进程去执行add,返回add的task任务对象
  12. const task = yield fork(add);
  13. // 等待有人向仓库派发 STOP 这个动作,如果有人派发了,就继续向下执行
  14. yield take(actionTypes.STOP);
  15. // 取消 task 任务执行(cancel不会阻塞,它执行后,会继续向下走)
  16. yield cancel(task);
  17. console.log('父saga继续向前执行');
  18. }
  19. // 根saga
  20. export default function* rootSaga(){
  21. yield addListener();
  22. }

src/components/Counter.js

  1. import React from 'react';
  2. import {connect} from 'react-redux';
  3. import actions from '../store/actions/counter.js';
  4. class Counter extends React.Component {
  5. render(){
  6. let ps = this.props;
  7. return (
  8. <div>
  9. <p>{ps.number}</p>
  10. <div>
  11. <button onClick={ps.stop}>停止</button>
  12. </div>
  13. </div>
  14. )
  15. }
  16. }
  17. export default connect(state => state, actions)(Counter)