1. redux-saga #

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

2. redux-saga工作原理 #

3. redux-saga分类 #

4. API #

5. redux-saga计数器 #

5.1 store/index.js #

import reducer from './reducer';
import {createStore,applyMiddleware,compose} from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas';
let sagaMiddelware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
let store = composeEnhancers(applyMiddleware(sagaMiddelware))(createStore)(reducer);
sagaMiddelware.run(rootSaga);
export default store;

5.2 src2/sagas.js #

import { put,takeEvery,all } from 'redux-saga/effects';
//import {delay} from 'redux-saga';
import * as types from './store/action-types';
import 'babel-polyfill'
//用于返回一个延迟 1 秒再 resolve 的 Promise
export const delay = ms => new Promise(resolve=>setTimeout(resolve,ms));

//我们的工作saga,将异步执行increment任务
export function* incrementAsync(){
    console.log('incrementAsync');
  //Sagas 被实现为 Generator 函数,它 yield 对象到 redux-saga middleware。 被 yield 的对象都是一类指令,指令可被 middleware 解释执行
  //当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成。
  yield delay(1000);
  //一旦 Promise 被 resolve,middleware 会恢复 Saga 去执行下一个语句(更准确地说是执行下面所有的语句,直到下一个 yield)
  // 调用 put意味着Saga 指示 middleware 发起一个 INCREMENT 的 action
  yield put({type:types.INCREMENT});
}

//我们监听saga,在每一个INCREMENT_ASYNC派生后,派发一个新的incrementAsync任务
export function* watchIncrementAsync(){
    yield takeEvery(types.INCREMENT_ASYNC,incrementAsync);
}
export function* hello(){
    console.log('hello');
}
export default function* rootState(){
    yield all([watchIncrementAsync()]);
}

6. 日志记录器 #

这个 Saga 将监听所有发起到 store 的 action,然后将它们记录到控制台

6.1 src2/sagas.js #

function * logger(getState,action){
    console.log('action',action);
    console.log('newState',getState());
}
function* watchAndLog(getState){
  yield takeEvery('*',logger,getState);
}
function* watchAndLog2(getState,action){
    while(true){
        //take 就像我们更早之前看到的 call 和 put。它创建另一个命令对象,告诉 middleware 等待一个特定的 action
        //正如在 call Effect 的情况中,middleware 会暂停 Generator,直到返回的 Promise 被 resolve
        //在 take 的情况中,它将会暂停 Generator 直到一个匹配的 action 被发起了
        //在 takeEvery 的情况中,被调用的任务无法控制何时被调用, 它们将在每次 action 被匹配时一遍又一遍地被调用。并且它们也无法控制何时停止监听。
        //而在 take 的情况中,控制恰恰相反。与 action 被 推向(pushed) 任务处理函数不同,Saga 是自己主动 拉取(pulling) action 的。
        const action = yield take('*');
        console.log('action',action);
        console.log('newState',getState());
    }
}
export function* watchIncrementAsync(){
    yield takeEvery(types.INCREMENT_ASYNC,incrementAsync);
}
export function* watchIncrementAsync2(getState){
    for(let i=0;i<3;i++){
        const action =  yield take(types.INCREMENT_ASYNC,incrementAsync);
    }
    yield put({type:types.MAX_NUMBER});

}

7. 登录注册 #

7.1 src3\sagas.js #

import 'babel-polyfill'
import { put,takeEvery,all,take,call,fork,cancel } from 'redux-saga/effects';
import { push } from 'react-router-redux'
import * as types from './store/action-types';
let Api = {
    login(username,password){
        return new Promise(function(resolve){
           setTimeout(function(){
               resolve('token');
           },1000);
        });
    },
    logout(){
        return new Promise(function(resolve){
            setTimeout(function(){
                resolve();
            },1);
        });
    }
}
function* login(dispatch,username,password){
  try{
      //如果 Api 调用成功了,login 将发起一个 LOGIN_SUCCESS action 然后返回获取到的 token。 如果调用导致了错误,将会发起一个 LOGIN_ERROR action
      const token = yield call(Api.login,username,password);
      yield put({type:types.LOGIN_SUCCESS,token});
      dispatch(push('/logout'));
  }catch(error){
      yield put({type:types.LOGIN_ERROR,error});
  }
}

function* logout(dispatch){
    try{
        yield call(Api.logout);
        yield put({type:types.LOGOUT_SUCCESS});
        dispatch(push('/login'));
    }catch(error){
        yield put({type:types.LOGOUT_ERROR,error});
    }
}

function* loginFlow(getState,dispatch){
    //loginFlow 在一个 while(true) 循环中实现它所有的流程,这样做的意思是:一旦到达流程最后一步(LOGOUT),通过等待一个新的 LOGIN_REQUEST action 来启动一个新的迭代。
    while(true){
        //loginFlow 首先等待一个 LOGIN_REQUEST action。 然后在 action 的 payload 中获取有效凭据(即 user 和 password)并调用一个 call 到 login 任务。
        //call 不仅可以用来调用返回 Promise 的函数。我们也可以用它来调用其他 Generator 函数
        //loginFlow 将等待 login 直到它终止或返回
        //redux-saga 提供了另一个 Effect:fork。 当我们 fork 一个 任务,任务会在后台启动,调用者也可以继续它自己的流程,而不用等待被 fork 的任务结束。
        let {username,password} = yield take(types.LOGIN_REQUEST);
        const task = yield fork(login,dispatch,username,password);
        yield take(types.LOGOUT_REQUEST);
        yield cancel(task);
        yield call(logout,dispatch);
    }
}
export default function* rootSaga({subscribe,dispatch,getState}){
    yield all([loginFlow(getState,dispatch)]);
}

7.2 src3\store\index.js #

import reducer from './reducer';
import {createStore,applyMiddleware,compose} from 'redux';
import createSagaMiddleware from 'redux-saga';
import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux';
import createHistory from 'history/createHashHistory';
const history = createHistory();
const router = routerMiddleware(history);
import rootSaga from '../sagas';
let sagaMiddelware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
let store = composeEnhancers(applyMiddleware(sagaMiddelware,router))(createStore)(reducer);
sagaMiddelware.run(rootSaga,{subscribe:store.subscribe, dispatch:store.dispatch, getState:store.getState});
window.store = store;
export default store;

7.3 src3/store/reducer.js #

import * as types from './action-types';
import {combineReducers} from 'redux';
import {routerReducer} from 'react-router-redux'
let user =  function(state={token:null,error:null},action){
    switch(action.type){
        case types.LOGIN_SUCCESS:
            return {...state,token:action.token};
        case types.LOGIN_ERROR:
            return {...state,error:action.error};
        case types.LOGOUT_SUCCESS:
            return {token:null,error:null};
        case types.LOGOUT_ERROR:
            return state;
        default:
            return state;
    }
}

export default combineReducers({
    user,
    router: routerReducer
})

7.4 src3/store/actions.js #

import * as types from './action-types';
export default {
    login(username,password){
        return {type:types.LOGIN_REQUEST,username,password};
    },
    logout(){
        return {type:types.LOGOUT_REQUEST};
    }
}

7.5 src3/store/action-types.js #

export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_ERROR = 'LOGIN_ERROR';

export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_ERROR = 'LOGOUT_ERROR';

7.6 src3/components/Login.js #

import React,{Component} from 'react';
import {connect} from 'react-redux';
import actions from '../store/actions';
class Counter extends Component{
    handleSubmit = (event)=>{
        event.preventDefault();
        let username = this.username.value;
        let password = this.password.value;
        this.props.login(username,password);
    }
    render(){
        return (
            <form onSubmit={this.handleSubmit}>
                用户名<input type="text" ref={input=>this.username = input}/><br/>
                密码<input type="text"  ref={input=>this.password = input}/><br/>
                <input type="submit"/>
            </form>
        )
    }
}
export default connect(
    state=>state.user,
    actions
)(Counter);

7.7 src3/components/Logout.js #

import React,{Component} from 'react';
import {connect} from 'react-redux';
import actions from '../store/actions';
class Counter extends Component{
    handleSubmit = ()=>{
        let username = this.username.value;
        let password = this.password.value;
        this.props.login(username,password);
    }
    render(){
        return (
            <div>
                token:{this.props.token}{this.props.error}
                <button onClick={this.props.logout}>退出</button>
            </div>
        )
    }
}
export default connect(
    state=>state.user,
    actions
)(Counter);

7.8 src3/index.js #

import React,{Component} from 'react';
import Login from './components/Login';
import Logout from './components/Logout';
import {Provider} from 'react-redux';
import ReactDOM from 'react-dom';
import store from './store';
import { Route,Redirect,Switch} from 'react-router-dom'
import { ConnectedRouter} from 'react-router-redux'
import createHistory from 'history/createHashHistory'
const history = createHistory();
ReactDOM.render((
    <Provider store={store}>
        <ConnectedRouter history={history}>
            <Switch>
                <Route path="/login" component={Login}/>
                <Route path="/logout" component={Logout}/>
                <Redirect to="/login"/>
            </Switch>
        </ConnectedRouter>
    </Provider>
),document.querySelector('#root'));

8. 同时执行多个任务 #

export default function* rootSaga({subscribe,dispatch,getState}){
    yield all([loginFlow(getState,dispatch)]);
}

9. 在多个 Effects 之间启动 race #

有时候我们同时启动多个任务,但又不想等待所有任务完成,我们只希望拿到 胜利者:即第一个被 resolve(或 reject)的任务。 race Effect 提供了一个方法,在多个 Effects 之间触发一个竞赛(race)。

function log(){
    return new Promise(function(resolve){
        setTimeout(function(){
            resolve();
        },1000)
    });
}
function* backgroundTask(){
    while(true){
        yield log();
        console.log(new Date().toLocaleString());
    }
}

function* watchStartTask(){
 while(true){
     yield take('START_TASK');
     yield race({
         task: call(backgroundTask),
         cancel: take('CANCEL_TASK')
     })
 }
}

export default function* rootSaga({subscribe,dispatch,getState}){
    yield all([loginFlow(getState,dispatch),watchStartTask()]);
}

10. 资源 #