1. redux-saga #

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

2. redux-saga工作原理 #

3. redux-saga分类 #

4. 计数器 #

4.1 index.js #

src/index.js

import React from 'react'
import ReactDOM from 'react-dom';
import Counter from './components/Counter';
import {Provider} from 'react-redux';
import store from './store';
ReactDOM.render(<Provider store={store}>
  <Counter/>
</Provider>,document.querySelector('#root'));

4.2 sagas.js #

src/sagas.js

import {put,take} from 'redux-saga/effects';
import * as types from './store/action-types';

export function* rootSaga() {
    for (let i=0;i<3;i++){
        yield take(types.INCREMENT_ASYNC);
        yield put({type:types.INCREMENT});
    }
    console.log('已经达到最大值');
}

4.3 Counter.js #

src/components/Counter.js

import React,{Component} from 'react'
import {connect} from 'react-redux';
import actions from '../store/action';
class Counter extends Component{
    render() {
        return (
            <div>
                <p>{this.props.number}</p>
                <button onClick={this.props.increment}>+</button>
            </div>
      )
  }
}
export default connect(
    state => state,
    actions
)(Counter);

4.4 index.js #

src/store/index.js

import {createStore, applyMiddleware} from 'redux';
import reducer from './reducer';
import createSagaMiddleware from 'redux-saga';
import {rootSaga} from '../saga';
let sagaMiddleware=createSagaMiddleware();
let store=applyMiddleware(sagaMiddleware)(createStore)(reducer);
sagaMiddleware.run(rootSaga);
window.store=store;
export default store;

4.5 actions.js #

src/store/actions.js

import * as types from './action-types';
export default {
    increment() {
        return {type:types.INCREMENT_ASYNC}
    }
}

4.6 action-types.js #

src/store/action-types.js

export const INCREMENT_ASYNC='INCREMENT_ASYNC';
export const INCREMENT='INCREMENT';

4.7 reducer.js #

rc/store/reducer.js

import * as types from './action-types';
export default function (state={number:0},action) {
    switch(action.type){
        case types.INCREMENT:
            return {number: state.number+1};
        default:
            return state;
    }
}

5. 实现take #

5.2 effects.js #

src\redux-saga\effects.js

export function take(actionType) {
    return {
        type:'take',
        actionType
    }
}

export function put(action) {
    return {
        type: 'put',
        action
    }
}

6.支持产出terator #

6.1 Symbol.iterator #

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 Iterator 的作用有三个:

原生具备 Iterator 接口的数据结构如下。

6.1.1 arr #

var arr = [1,2];
let iterator = arr[Symbol.iterator]();
console.log(iterator.next());  //{ value: 1, done: false }
console.log(iterator.next());  //{ value: 2, done: false }
console.log(iterator.next());  //{ value: undefined, done: true }

6.1.2 对象 #

let obj = {
    name:'zhufeng',
    age:10,
    [Symbol.iterator]() {
        const self = this;
        let values = Object.values(self);
        let index = 0;
        return {
            next() {
                return {value:values[index++],done:index>values.length?true:false};
            }
        };
    }
}
let iterator = obj[Symbol.iterator]();
console.log(iterator.next());  //{ value: 1, done: false }
console.log(iterator.next());  //{ value: 2, done: false }
console.log(iterator.next());  //{ value: undefined, done: true }

6.2 saga.js #

import {put,take} from '../redux-saga/effects';
import * as types from './action-types';

+export function* increment() {
+    yield put({type:types.INCREMENT});
+}

export function* rootSaga() {
    for (let i=0;i<3;i++){
        yield take(types.INCREMENT_ASYNC);
+        yield increment();
    }
    console.log('已经达到最大值');
}

6.2 redux-saga/index.js #

export default function createSagaMiddleware() {
    function createChannel() {
        let listener={};
        function subscribe(actionType,cb) {
            listener[actionType]=cb;
        }
        function publish(action) {
            if (listener[action.type]) {
                let temp=listener[action.type];
                delete listener[action.type];
                temp(action);
            }
        }
        return {subscribe,publish};
    }
    let channel=createChannel();
    function sagaMiddleware({getState,dispatch}) {
        function run(generator) {
+            let it= typeof generator == 'function'? generator():generator;
            function next(action) {
                let {value:effect,done} = it.next(action);
                if (!done) {
+                    if (typeof effect[Symbol.iterator]=='function') {
+                        run(effect);
+                        next();
+                    } else {
                        switch (effect.type) {
                            case 'TAKE':
                                channel.subscribe(effect.actionType,next);
                                break;
                            case 'PUT':
                                dispatch(effect.action);
                                next();
                                break;
                            default:
                        }
                    }
                }
            }
            next();
        }
        sagaMiddleware.run=run;
        return function (next) {
            return function (action) {
                channel.publish(action);
                next(action);
            }
        }
    }
    return sagaMiddleware;
}

7. 支持takeEvery #

const task = yield fork(otherSaga, ...args)

7.1 saga.js #

import {put,takeEvery} from '../redux-saga/effects';
import * as types from './action-types';
export function* increment() {
    yield put({type:types.INCREMENT});
}

export function* rootSaga() {
+    yield takeEvery(types.INCREMENT_ASYNC,increment);
}

7.2 effects.js #

src\redux-saga\effects.js

export function take(actionType) {
    return {
        type:'TAKE',
        actionType
    }
}

export function put(action) {
    return {
        type: 'PUT',
        action
    }
}

export function fork(task) {
    return {
        type: 'FORK',
        task
    }
}

export function* takeEvery(actionType,task) {
    yield fork(function* () {
        while (true) {
            yield take(actionType);
            yield task();
        }
    });
}

7.3 redux-saga/index.js #

                        switch (effect.type) {
                            case 'TAKE':
                                channel.subscribe(effect.actionType,next);
                                break;
                            case 'PUT':
                                dispatch(effect.action);
                                next();
                                break;
+                            case 'FORK':
+                                run(effect.task);
+                                next();    
+                                break;
                            default:
                                break;
                        }

8. 支持promise #

8.1 saga.js #

import {put,takeEvery} from '../redux-saga/effects';
import * as types from './action-types';
+const delay=ms => new Promise((resolve,reject) => {
+    setTimeout(() => {
+        resolve();
+    },ms);
+});
export function* increment() {
+    yield delay(1000);
    yield put({type:types.INCREMENT});
}

export function* rootSaga() {
    yield takeEvery(types.INCREMENT_ASYNC,increment);
}

8.2 redux-saga/index.js #

function next(action) {
                let {value:effect,done} = it.next(action);
                if (!done) {
                    if (typeof effect[Symbol.iterator]=='function') {
                        run(effect);
                        next();
+                    } else if(effect.then){
+                        effect.then(next);
+                    }else {
                        switch (effect.type) {
                            case 'TAKE':
                                channel.subscribe(effect.actionType,next);
                                break;

9. 支持 call #

9.1 sagas.js #

export function* increment() {
    //yield delay(1000);
    yield call(delay,1000);
    yield put({type:types.INCREMENT});
}

9.2 effects.js #

export function call(fn,...args) {
    return {
        type: 'CALL',
        fn,
        args
    }
}

9.3 saga.js #

    case 'CALL':
        effect.fn(...effect.args).then(next);
    break;

10. 支持 delay #

10.1 sagas.js #

+import {put,takeEvery,delay} from '../redux-saga/effects';
import * as types from './action-types';

export function* increment() {
+    yield delay(1000);
    yield put({type:types.INCREMENT});
}

export function* rootSaga() {
    yield takeEvery(types.INCREMENT_ASYNC,increment);
}

10.2 effects.js #

const innerDelay=ms => new Promise((resolve,reject) => {
    setTimeout(() => {
        resolve();
    },ms);
});
export function delay(...args) {
    return call(innerDelay,...args);
}

11. 支持 cps #

11.1 sagas.js #

import {put,takeEvery,call,cps} from '../redux-saga/effects';
import * as types from './action-types';

+const delay = (ms,callback)=>{
+    setTimeout(() => {
+        callback('ok');
+    },ms);
+}

+export function* increment() {
+    let data = yield cps(delay,1000);
+    console.log(data);
+    yield put({type:types.INCREMENT});
+}

export function* rootSaga() {
    yield takeEvery(types.INCREMENT_ASYNC,increment);
}

11.2 effects.js #

export function cps(fn,...args) {
    return {
        type: 'CPS',
        fn,
        args
    }
}

11.3 saga.js #

    case 'CALL':
        effect.fn(...effect.args).then(next);    
        break;
+    case 'CPS':
+        effect.fn(...effect.args,next);
+        break;
    default:
        break;

12. 支持all #

12.1 saga #

src\store\saga.js

import {put,takeEvery,delay,all} from '../redux-saga/effects';
import * as types from './action-types';

export function* increment() {
    yield delay(1000);
    yield put({type:types.INCREMENT});
}
export function* incrementWatcher() {
    yield takeEvery(types.INCREMENT_ASYNC,increment);
}
export function* logger() {
    console.log('action');
}
export function* loggerWatcher() {
    yield takeEvery(types.INCREMENT_ASYNC,logger);
}

export function* rootSaga() {
    yield all([incrementWatcher(),loggerWatcher()]);
    console.log('done');
}

12.2 effects #

export function all(fns) {
    return {
        type: 'ALL',
        fns
    }
}

12.3 redux-saga/index.js #

export default function createSagaMiddleware() {
    function createChannel() {
        let listener={};
        function subscribe(actionType,cb) {
            listener[actionType]=cb;
        }
        function publish(action) {
            if (listener[action.type]) {
                let temp=listener[action.type];
                delete listener[action.type];
                temp(action);
            }
        }
        return {subscribe,publish};
    }
    let channel=createChannel();
+    function times(cb,total) {
+        let count=0;
+        return function () {
+            if (++count === total) {
+                cb();
+            }
+        }
+    }
    function sagaMiddleware({getState,dispatch}) {
+        function run(generator,callback) {
            let it= typeof generator == 'function'? generator():generator;
            function next(action) {
                let {value:effect,done} = it.next(action);
                if (!done) {
                    if (typeof effect[Symbol.iterator]=='function') {
                        run(effect);
                        next();
                    } else if(effect.then){
                        effect.then(next);
                    }else {
                        switch (effect.type) {
                            case 'TAKE':
                                channel.subscribe(effect.actionType,next);
                                break;
                            case 'PUT':
                                dispatch(effect.action);
                                next();
                                break;
                            case 'FORK':
                                run(effect.task);
                                next();    
                                break;
                            case 'CALL':
                                effect.fn(...effect.args).then(next);    
                                break;
+                            case 'ALL':
+                                let fns=effect.fns;
+                                let done=times(next,fns.length);
+                                for (let i=0;i<fns.length;i++){
+                                    let fn=fns[i];
+                                    run(fn,done);
+                                }
+                                break;    
                            case 'CPS':
                                effect.fn(...effect.args,next);
                                break;
                            default:
                                break;
                        }
                    }
+                } else {
+                    callback&&callback();
+                }
            }
            next();
        }
        sagaMiddleware.run=run;
        return function (next) {
            return function (action) {
                channel.publish(action);
                next(action);
            }
        }
    }
    return sagaMiddleware;
}

13. 取消任务 #

13.1 saga #

src\store\saga.js

import {put,take,delay,all,fork,cancel} from '../redux-saga/effects';
import * as types from './action-types';

export function* increment() {
    while(true){
        yield delay(1000);
        yield put({type:types.INCREMENT});
    }
}

export function* incrementWatcher() {
    const task = yield fork(increment);
    yield take(types.STOP_INCREMENT);
    yield cancel(task);
}

export function* rootSaga() {
    yield all([incrementWatcher()]);
    console.log('done');
}

13.2 Counter.js #

src\components\Counter.js

import React,{Component} from 'react'
import {connect} from 'react-redux';
import actions from '../store/action';
class Counter extends Component{
    render() {
        return (
            <div>
                <p>{this.props.number}</p>
                <button onClick={this.props.increment}>+</button>
                <button onClick={this.props.stop}>stop</button>
            </div>
      )
  }
}
export default connect(
    state => state,
    actions
)(Counter);

13.3 effects.js #

src\redux-saga\effects.js

export function cancel(task) {
    return {
        type: 'CANCEL',
        task
    }
}

13.4 index.js #

src\redux-saga\index.js

function sagaMiddleware({getState,dispatch}) {
        function run(generator,callback) {
            let it= typeof generator == 'function'? generator():generator;
            function next(action) {
                let {value:effect,done} = it.next(action);
                if (!done) {
                    if (typeof effect[Symbol.iterator]=='function') {
                        run(effect);
                        next();
                    } else if(effect.then){
                        effect.then(next);
                    }else {
                        switch (effect.type) {
                            case 'TAKE':
                                channel.subscribe(effect.actionType,next);
                                break;
                            case 'PUT':
                                dispatch(effect.action);
                                next();
                                break;
+                            case 'FORK':
+                                let newTask = effect.task();
+                                run(newTask);
+                                next(newTask);
+                                break;
                            case 'CALL':
                                effect.fn(...effect.args).then(next);    
                                break;
                            case 'ALL':
                                let fns=effect.fns;
                                let done=times(next,fns.length);
                                for (let i=0;i<fns.length;i++){
                                    let fn=fns[i];
                                    run(fn,done);
                                }
                                break;    
                            case 'CPS':
                                effect.fn(...effect.args,next);
                                break;
+                            case 'CANCEL':
+                                effect.task.return('over');    
+                                break;
                            default:
                                break;
                        }
                    }
                } else {
                    callback&&callback();
                }
            }
            next();
        }
        sagaMiddleware.run=run;
        return function (next) {
            return function (action) {
                channel.publish(action);
                next(action);
            }
        }
    }

14. 资源 #