Redux-saga原理

204 阅读7分钟

1.redux-saga是用来干嘛的

  • redux-saga 是redux 的中间件,为redux提供额外的操作。
  • 在reducers 中的所有操作都是同步并且是纯粹的,Reducer是纯函数。纯函数就是不会产生对外部产生副作用,你传给他什么,他就给你吐出来什么。
  • 但是在实际开发中,我们希望请求一些异步函数且不纯粹的操作(改变外部的状态),这写在函数式编程中称为副作用函数。

2.redux-saga工作原理

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

3.redux-saga分类

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

4.使用 redux-saga

npx create-react-app react-redux-saga

pnpm i redux redux-saga react-redux
  • saga/index.js
import { createStore } from 'redux'
import { applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { userReducer } from './reducers'
import { rootSaga } from './saga'

let sagaMiddleWare = createSagaMiddleware()
let store = applyMiddleware(sagaMiddleWare)(createStore)(userReducer)

sagaMiddleWare.run(rootSaga)

export default store;
  • saga/actions-type.js
export const INCREMENT_ASYNC = 'INCREMENT_ASYNC';
export const INCREMENT = 'INCREMENT';
  • saga/reducers.js
import * as types from './actions-type'

const initData = {
  userInfo: {},
  number: 0,

}
const userReducer = (userData = initData, action) => {

  switch (action.type) {
    case types.INCREMENT_ASYNC:
      return { ...userData, number: userData.number + 1 }
    default:
      return userData;
  }
}

export {
  userReducer
}
  • saga/saga.js
import { put, take } from 'redux-saga/effects';
import * as types from './actions-type';

export function* rootSaga() {
  // for (let i = 0; i < 3; i++) {
  //   yield take(types.INCREMENT_ASYNC);
  //   yield put({ type: types.INCREMENT });
  // }
  yield take(types.INCREMENT_ASYNC);
  console.log('已经达到最大值');
}
  • index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { Provider } from 'react-redux'
import store from './redux-saga';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);
  • App.js
import logo from './logo.svg';
import './App.css';
import { useSelector, useDispatch } from 'react-redux';
import { INCREMENT_ASYNC } from "./redux-saga/actions-type";
import { useEffect } from "react";
function App() {
  const dispatch = useDispatch();
  const data = useSelector((state) => state.number);
  console.log(data)
  useEffect(() => {
    dispatch({ type: INCREMENT_ASYNC });
  }, [dispatch]);
  return (
    <div className="App">
      {data}
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

5.Redux-saga 原理

5.1.实现take put原理

  • take 主要是用于监听一个特定的action的effect,会等待指定的action被dispatch,然后执行后续的操作
  • put 是一个 effect,用于触发一个 Redux action,类似于 dispatch。它的主要作用是在 saga 中发出 action,通常用于更新 Redux store 或触发其他的副作用
  • redux-saga/index.ts
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=generator();
          function next(action) {
              let {value:effect,done} = it.next(action);
              if (!done) {
                  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;
}
  • redux-saga/effects.ts
export function take(actionType) {
  return {
    type: 'take',
    actionType
  }
}

export function put(action) {
  return {
    type: 'put',
    action
  }
}
  • redux-saga/effects-actions.ts
export const TAKE = 'TAKE'
export const PUT = 'PUT'

5.2.支持产出 Iterator

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;
}

5.3.支持 takeEvery fork

  • takeEvery 是一个用来监听并处理所有指定类型 action 的 effect。它的主要作用是每当指定的 action 被 dispatch 时,都启动一个新的 saga 执行,而不会阻塞其他操作。和 take 不同,takeEvery 会启动多个并行的任务(即使前一个任务还没有完成)。
  • 处理并发的异步操作:当多个相同的 action 被 dispatch 时,takeEvery 会并行地启动多个任务来处理这些 action,而不需要等待前一个任务完成。
  • fork 是用来创建一个新的并行任务,并立即执行,但是不会阻塞当前的执行流。
  • redux-saga/effects.ts
import { FORK, PUT, TAKE } from "./effects-actions"

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();
    }
  });
}
import { FORK } from "./effects-actions";

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 = generator();
      function next(action?) {
        let { value: effect, done } = it.next(action);
        if (!done) {
          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:
          }
        }
      }
      next();
    }
    sagaMiddleware.run = run;
    return function (next) {
      return function (action) {
        channel.publish(action);
        next(action);
      }
    }
  }
  return sagaMiddleware;
}

5.4.支持Promise

import { FORK } from "./effects-actions";

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 = 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;
              default:
            }
          }

        }
      }
      next();
    }
    sagaMiddleware.run = run;
    return function (next) {
      return function (action) {
        channel.publish(action);
        next(action);
      }
    }
  }
  return sagaMiddleware;
}

5.5.支持call

  • 执行异步操作并获取结果call 用来调用异步函数,等待结果,并且可以处理返回值或错误。
  • 同步处理异步操作:通过 call,你可以把异步操作的处理方式转变为同步流程,使代码结构更清晰。
  • redux-saga/effects.ts
export function call(fn, ...args) {
  return {
    type: CALL,
    fn,
    args
  }
}
  • redux-saga/effects-actions.ts
import { CALL, FORK, PUT, TAKE } from "./effects-actions";

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 = 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;
              default:
            }
          }

        }
      }
      next();
    }
    sagaMiddleware.run = run;
    return function (next) {
      return function (action) {
        channel.publish(action);
        next(action);
      }
    }
  }
  return sagaMiddleware;
}

5.6.支持cps

  • cps 用于处理需要通过回调来获取结果的异步操作。
  • redux-saga/effects.js
export function cps(fn, ...args) {
  return {
    type: CPS,
    fn,
    args
  }
}
import { CALL, CPS, FORK, PUT, TAKE } from "./effects-actions";

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 = 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 CPS:
                effect.fn(...effect.args, next);
                break;
              default:
            }
          }

        }
      }
      next();
    }
    sagaMiddleware.run = run;
    return function (next) {
      return function (action) {
        channel.publish(action);
        next(action);
      }
    }
  }
  return sagaMiddleware;
}

5.7.支持all

  • all 是一个效果方法,用于并行执行多个 saga(生成器函数)。它的作用是让多个异步操作(或多个 saga)并行运行,而不需要等待每个操作逐个完成
  • redux-saga/effects.ts
export function all(fns) {
  return {
    type: ALL,
    fns
  }
}
  • redux-saga/index.ts
import { ALL, CALL, CPS, FORK, PUT, TAKE } from "./effects-actions";

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 = 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 CPS:
                effect.fn(...effect.args, 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;
              default:
            }
          }
        } else {
          callback && callback();
        }
      }
      next();
    }
    sagaMiddleware.run = run;
    return function (next) {
      return function (action) {
        channel.publish(action);
        next(action);
      }
    }
  }
  return sagaMiddleware;
}

5.8.取消任务

  • redux-saga/effects.ts
export function cancel(task) {
  return {
    type: CANCEL,
    task
  }
}
import { ALL, CALL, CANCEL, CPS, FORK, PUT, TAKE } from "./effects-actions";

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 = 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 FORK:
                //   run(effect.task);
                //   next();
                break;
              case CALL:
                effect.fn(...effect.args).then(next);
                break;
              case CPS:
                effect.fn(...effect.args, 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 CANCEL:
                effect.task.return('over');
                break;
              default:
            }
          }
        } else {
          callback && callback();
        }
      }
      next();
    }
    sagaMiddleware.run = run;
    return function (next) {
      return function (action) {
        channel.publish(action);
        next(action);
      }
    }
  }
  return sagaMiddleware;
}