一起来学点redux-saga

1.概述

Redux-saga是一个用于管理 Redux 应用异步操作的中间件(又称异步action)

本质都是为了解决异步action的问题

Redux Saga可以理解为一个和系统交互的常驻进程,这个线程可以通过正常的Redux Action从主应用程序启动,暂停和取消,它能访问完整的Redux state,也可以dispatch Redux Action。 一个 Saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。

其中,Saga可简单定义如下的公式:

Saga = Worker + Watcher

2.简单使用

  • redux-saga本质是一个可以自执行的generator。
  • 在 redux-saga 中,UI 组件自身从来不会触发任务,它们总是会 dispatch 一个 action 来通知在 UI 中哪些地方发生了改变,而不需要对 action 进行修改。redux-saga 将异步任务进行了集中处理,且方便测试
  • 所有的东西都必须被封装在 sagas 中。sagas 包含3个部分,用于联合执行任务:
    • worker saga 做所有的工作,如调用 API,进行异步请求,并且获得返回结果
    • watcher saga监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务
    • root saga立即启动 sagas 的唯一入口
特别提醒: redux-saga是通过ES6中的generator实现的。

下面来创建一个redux-saga例子

  • 安装npm install redux-saga –g;
  • 使用createSagaMiddleware方法创建saga的sagaMiddleware
  • 在创建的redux的store时,使用applyMiddleware函数将创建的saga Middleware实例绑定到store上
  • 最后需要运行sagaMiddleware

创建一个hellosaga.js文件

export function * helloSaga() {
  console.log('Hello Sagas!');
}

在redux项目中使用redux-saga中间件

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore, applyMiddleware,combineReducers } from 'redux';
import rootReducer from './reducers';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import { Provider } from 'react-redux';
import { watchIncrementAsync } from './sagas/counter';
import { helloSaga } from './sagas'

//====1 创建一个saga中间件
const sagaMiddleware = createSagaMiddleware();

//====2 创建store
const store = createStore(
  rootReducer,
  composeWithDevTools(
    applyMiddleware(sagaMiddleware)
  )
);
//==== 3动态执行saga,注意:run函数只能在store创建好之后调用
sagaMiddleware.run(helloSaga);

ReactDOM.render(
  <Provider store={ store }>
    <App />
  </Provider>,
  document.getElementById('root')
);

这样代码跑起来,就可以看到控制台输出了Hello Saga

和调用redux的其他中间件一样,如果想使用redux-saga中间件,那么只要在applyMiddleware中调用一个createSagaMiddleware的实例。唯一不同的是需要调用run方法使得generator可以开始执行。

3.运行流程图

image

由上图可以看书saga主要做的了三件事

  • 监听用户发出的Action。
  • 发现用户发出的Action是自己当前的Action,然后做一些副作用(派发一个新的任务)。
  • store接收到新的任务,返回新的state。

4.核心AIP

tackEvery

监听action,每监听到一个action,就执行一次操作

允许多个请求同时执行,不管之前是否还有一个或多个请求尚未结束。

import { takeEvery } from 'redux-saga'

function* incrementAsync() {
    // 延迟1s
    yield delay(1000)

    yield put({
       type: 'increment'
    }) 
}

// 监听到Action为incrementAsync就会出发incrementAsync函数
function* watchIncrementAsync() {
    yield takeEvery('incrementAsync', incrementAsync)
}

// 注意watchIncrementAsync这个函数必须在主入口index中运行sagaMiddleware.run(watchIncrementAsync);

takeLatest

监听action,监听到多个action,只执行最近的一次

作用同takeEvery一样,唯一的区别是它只关注最后,也就是最近一次发起的异步请求,如果上次请求还未返回,则会被取消。

function* watchIncrementAsync() {
  yield takeLatest('incrementAsync', fetchData)
}

call

异步阻塞调用

用来调用异步函数,将异步函数和函数参数作为call函数的参数传入,返回一个js对象。saga引入他的主要作用是方便测试,同时也能让我们的代码更加规范化。

和js原生的call一样,call函数也可以指定this对象,只要把this对象当第一个参数传入call方法就

const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

function* fetchData() { 
    // 2秒后打印saga(阻塞)
    // yield delay(2000);
    yield call(delay,2000); 
    console.log('saga'); 
}

  // 加了call和不加效果是一样的,saga引入他的主要作用是方便测试,同时也能让我们的代码更加规范化。

fork

异步非阻塞调用,无阻塞的执行fn,执行fn时,不会暂停Generator

非阻塞任务调用机制:上面我们介绍过call可以用来发起异步操作,但是相对于 generator 函数来说,call 操作是阻塞的,只有等 promise 回来后才能继续执行,而fork是非阻塞的 ,当调用 fork 启动一个任务时,该任务在后台继续执行,从而使得我们的执行流能继续往下执行而不必一定要等待返回。

const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

function* fetchData() { 
    // 不用等待2秒,直接可以打印出saga,并发执行
    yield fork(delay,2000); 
    console.log('saga'); 
}

put

相当于dispatch,分发一个action

yield put({ type: 'incrementAsync'})

select

相当于getState,用于获取store中相应部分的state

function* incrementAsync(action) {
  let state = yield select(state => console.log('-----',state))
}

tack

监听action,暂停Generator,匹配的action被发起时,恢复执行。

export function* watchIncrementAsync() {
   while(true){
     yield take('INCREMENT_ASYNC'); // 监听
     yield fork(incrementAsync);
  }
  // yield takeLatest(INCREMENT_ASYNC, incrementAsync); //takeLatest
}

cancel

创建一个Effect描述信息,针对 fork 方法返回的 task ,可以进行取消关闭。cancel(task)

race([...effects])

创建一个Effect描述信息,指示 middleware 在多个 Effect 之间运行一个 race(与 Promise.race([...]) 的行为类似)。

race可以取到最快完成的那个结果,常用于请求超时

all([]...effects)

创建一个 Effect 描述信息,指示 middleware 并行运行多个 Effect,并等待它们全部完成。这是与标准的Promise#all相对应的 API。

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


// 正确写法, effects 将会同步执行
const [userInfo, repos] = yield [
  call(fetch, '/users'),
  call(fetch, '/repos')
];

// 这两个请求是并行的

5.Redux-saga使用案例

下面是这个简单demo的目录结构

包含了同步,异步,网络请求,希望这个简单的demo带你学会redux-saga

image
  • index.js

入口文件

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore, applyMiddleware,combineReducers } from 'redux';
import rootReducer from './reducers';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import { Provider } from 'react-redux';
import rootSage from './sagas';


//====1 创建一个saga中间件
const sagaMiddleware = createSagaMiddleware();


//====2 创建store
const store = createStore(
  rootReducer,
  composeWithDevTools(
    applyMiddleware(sagaMiddleware)
  )
);
//==== 3动态执行saga,注意:run函数只能在store创建好之后调用
sagaMiddleware.run(rootSage);


ReactDOM.render(
  <Provider store={ store }>
    <App />
  </Provider>,
  document.getElementById('root')
);
  • App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increment,incrementAsync,decrement } from './actions/counter';
import './app.css'
import { get_user } from './actions/user';
class App extends Component {

  constructor(props){
    super(props);
  }

  render() {
    const { message } = this.props.user;
    return (
      <div className="App">
          <span className='count'>{ this.props.counter }</span>
          <br />
          <button onClick={ this.props.increment }>同步+1</button>
          <button onClick={ this.props.decrement }>同步-1</button>
          <button onClick={ this.props.incrementAsync }>异步</button>
          <button onClick={ this.props.get_user }>网络请求</button>
          <h1>{ message }</h1>
      </div>
    );
  }
}


//映射组件props的数据部分
const mapStateToProps = (state) => {
  return {
    counter: state.counter,
    user: state.user
  };
};
//映射组件props的函数部分
// const  mapDispatchToProps = (dispatch) => {
//   return {
//     increment:(dispatch)=>{dispatch(increment)}
//   }
// };


export default connect(mapStateToProps, { increment,incrementAsync,decrement,get_user })(App);
  • actions/counter.js
export const INCREMENT = 'INCREMENT';
export const INCREMENT_ASYNC = 'INCREMENT_ASYNC';
export const DECREMENT = 'DECREMENT'


//count+1
export const increment = () => {
  return {
    type: INCREMENT
  }
};


//count-1
export const decrement = () => {
  return {
    type:DECREMENT
  }
}

//异步增加
export const incrementAsync = () => {
  return {
    type: INCREMENT_ASYNC
  }
};

  • actions/user.js
export const get_user = () => {
    return {
      type: 'FETCH_REQUEST'
    }
};
  • reducers/index.js
import { combineReducers } from 'redux';


import counter from './counter';
import user from './user';

// 合并所有的reduces
export default combineReducers({
  counter,
  user
});
  • reducers/counter.js
import { INCREMENT , DECREMENT} from '../actions/counter';

const counter = (state = 1, action ) => {
  switch(action.type) {
    case INCREMENT:
      return state + 1;
    case DECREMENT: {
        return state-1
      }
    default: return state;
  }
}

export default counter;
  • reducers/user.js
const initialState = {
    message: '等待',
    age:'20'
  };
  
  const user = (state = initialState, action) => {
    switch(action.type) {
      case "FETCH_REQUEST":
        return {
          ...state,
          message: '请求中'
        }
      case "FETCH_SUCCEEDED":
        return {
            ...state,
            message: '詹姆斯'
        }
      case "FETCH_FAILURE":
        return {
           ...state,
            message: '请求失败'
        }
      default: return state;
    }
  }
  
  export default user;
  • sagas/index.js
import { all } from 'redux-saga/effects';

import { counterSagas } from './counter';
import { userSagas } from './user';

// 合并所有需要监听的saga
export default function* rootSage() {
  
  yield all([
    ...counterSagas,
    ...userSagas
  ])
}
  • sagas/counter.js
//import { delay } from 'redux-saga';
import { 
  takeEvery, call, put,take,fork,takeLatest,select,all
} from 'redux-saga/effects';
import { INCREMENT_ASYNC ,INCREMENT_TAKE,DECREMENT} from '../actions/counter';

const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

function* incrementAsync(action) {

  let state = yield select(state => console.log(state))

  //yield fork(delay,2000);
  //yield delay(2000);
  yield call(delay,2000)
  yield put({ type: 'INCREMENT' })
  //yield fork(()=>{return put({ type: 'INCREMENT' })});
  
  // yield all([
  //   //call(delay,2000),
  //   yield put({ type: 'INCREMENT',data:'9898' }),
  //   yield put({ type: 'INCREMENT--' ,data:'000'}),
  //   yield put({ type: 'INCREMENT----' })
  // ])
  //同步的方式来写异步代码
  // yield put({ type: 'INCREMENT' });

}

export function* watchIncrementAsync() {
   while(true){
     yield take('INCREMENT_ASYNC');
     yield fork(incrementAsync);
     yield fork(()=>{console.log('--------')})
     yield fork(()=>{console.log('--------')})
  }
  yield takeLatest(INCREMENT_ASYNC, incrementAsync); //takeLatest
}

export const counterSagas = [
  //fork(()=>{console.log('---------')}),
  watchIncrementAsync(),
  watchIncrementAsync(),
  watchIncrementAsync(),
]
  • sagas/user.js
import { takeEvery, call, put,all } from 'redux-saga/effects';
import axios from 'axios';
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
function* fetchUser() {
  try {
    //axios.get('https://jsonplaceholder.typicode.com/users')
    const user = yield call(axios.get, "https://jsonplaceholder.typicode.com/users");
    yield put({type: "FETCH_SUCCEEDED"})
  } catch(e) {
    yield put({type: "FETCH_FAILURE"});
  }
}

function* watchFetchUser() {
  yield all([
     takeEvery('FETCH_REQUEST', fetchUser), // 监听发出Action为FETCH_REQUEST,然后出发请求函数fetchUser
  ])
  
}

export const userSagas = [
  watchFetchUser()
]
  • 最后运行的效果图如下:
image

6.总结

  • redux-saga就是一个redux的中间件,用于更优雅的管理异步
  • redux-saga有一堆的api可供使用
  • 可以利用同步的方式处理异步逻辑,便于捕获异常,易于测试;

优点:

(1)副作用转移到单独的saga.js中,不再掺杂在action.js中,保持 action 的简单纯粹,又使得异步操作集中可以被集中处理。对比redux-thunk

(2)redux-saga 提供了丰富的 Effects,以及 sagas 的机制(所有的 saga 都可以被中断),在处理复杂的异步问题上更顺手。提供了更加细腻的控制流。

(3)对比thunk,dispatch 的参数依然是一个纯粹的 action (FSA)。

(4)每一个 saga 都是 一个 generator function,代码可以采用 同步书写 的方式 去处理 异步逻辑(No Callback Hell),代码变得更易读。

(5)同样是受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理。

缺点:

(1)generator 的调试环境比较糟糕,babel 的 source-map 经常错位,经常要手动加 debugger 来调试。

(2)redux-saga 不强迫我们捕获异常,这往往会造成异常发生时难以发现原因。因此,一个良好的习惯是,相信任何一个过程都有可能发生异常。如果出现异常但没有被捕获,redux-saga 的错误栈会给你一种一脸懵逼的感觉。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容