一起来学redux-sage
1.概述
Redux-saga
是一个用于管理 Redux
应用异步操作的中间件(又称异步action
)
本质都是为了解决异步action的问题
Redux Saga可以理解为一个和系统交互的常驻进程,这个线程可以通过正常的Redux Action从主应用程序启动,暂停和取消,它能访问完整的Redux state,也可以dispatch Redux Action。 一个 Saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。
其中,Saga可简单定义如下的公式:
Saga = Worker + Watcher
中间件
中间件就是非业务的技术类组件。它介于底层逻辑与业务之间,相当于中介的作用。(如果不了解中间件可以查看上一篇公众号文章《带你了解redux与react-redux》)
2.使用
注意:⚠️redux-saga是通过ES6中的generator实现的。
1.redux-saga本质是一个可以自执行的generator。
2.在 redux-saga 中,UI 组件自身从来不会触发任务,它们总是会 dispatch 一个 action 来通知在 UI 中哪些地方发生了改变,而不需要对 action 进行修改。redux-saga 将异步任务进行了集中处理,且方便测试
3.所有的东西都必须被封装在 sagas 中。sagas 包含3个部分,用于联合执行任务:
worker saga 做所有的工作,如调用 API,进行异步请求,并且获得返回结果
watcher saga监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务
root saga立即启动 sagas 的唯一入口
下面来创建一个redux-saga例子
(1)安装npm install redux-saga –g;
(2)使用createSagaMiddleware方法创建saga的sagaMiddleware
(3)在创建的redux的store时,使用applyMiddleware函数将创建的saga Middleware实例绑定到store上
(4)最后需要运行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.运行流程图
由上图可以看书saga主要做的了三件事
- 监听用户发出的Action。
- 发现用户发出的Action是自己当前的Action,然后做一些副作用(派发一个新的任务)。
- store接收到新的任务,返回新的state。
4.核心AIP
(1)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);
(2)takeLatest
监听action,监听到多个action,只执行最近的一次
作用同takeEvery一样,唯一的区别是它只关注最后,也就是最近一次发起的异步请求,如果上次请求还未返回,则会被取消。
function* watchIncrementAsync() { yield takeLatest('incrementAsync', fetchData) }
(3)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引入他的主要作用是方便测试,同时也能让我们的代码更加规范化。
(4)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'); }
(5)put
相当于dispatch,分发一个action
yield put({ type: 'incrementAsync'})
(6)select
相当于getState,用于获取store中相应部分的state
function* incrementAsync(action) { let state = yield select(state => console.log('-----',state)) }
(7)tack
监听action,暂停Generator,匹配的action被发起时,恢复执行。
export function* watchIncrementAsync() { while(true){ yield take('INCREMENT_ASYNC'); // 监听 yield fork(incrementAsync); } // yield takeLatest(INCREMENT_ASYNC, incrementAsync); //takeLatest }
(8)cancel
创建一个Effect描述信息,针对 fork 方法返回的 task ,可以进行取消关闭。cancel(task)
(9)race([...effects])
创建一个Effect描述信息,指示 middleware 在多个 Effect 之间运行一个 race(与 Promise.race([...]) 的行为类似)。
race可以取到最快完成的那个结果,常用于请求超时
(10)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
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'; function* incrementAsync(action) { yield call(delay,2000) yield put({ type: 'INCREMENT' }) } export function* watchIncrementAsync() { // while(true){ // yield take('INCREMENT_ASYNC'); // yield fork(incrementAsync); // } yield takeLatest(INCREMENT_ASYNC, incrementAsync); //takeLatest } export const counterSagas = [ 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() ]
最后运行的效果图如下:
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 的错误栈会给你一种一脸懵逼的感觉。