Redux相关笔记之三

235 阅读9分钟

前言

本文是Redux相关笔记的最后一篇,主要内容为redux-saga。

前置知识---Generator函数与

redux-saga主要采用了Generator函数也就是生成器。Generator函数的作用是可以暂停执行,再次执行的时候会从上一次暂停的地方继续执行。

本质上其实就是一个迭代器,多说无益,我们直接上手体验一下。

Generator函数,需要使用星号来表示,yield关键字表示产出,每次执行遇到yield就会停止,直到调用next。

function* saga() {
  console.log('start1');
  yield 1;
  console.log('start2');
  yield 2;
}

这里我们写了一个很基本的Generator函数,我们调用一次这个函数,它就会返回一个迭代器,这个迭代器包含next方法。调用next方法就会执行Generator函数里面的代码,直到进行产出或结束就停止。

const it = saga();
// 上面调用了saga,控制台不会有任何反应,因为Generator函数里面的内容并没有执行
const result1 = it.next();
console.log(result1);

这里我们调用了Generator函数拿到了迭代器,然后执行迭代器的next方法,接受并打印了返回值为result1,此时控制台的输出会是什么?

image.png

是的,输出的是start1和result1,我们调用了next,它执行完yield获取到了产出就停止了,需要继续执行next方法才会继续。

然后我们关注一下result1的数据形式,它是一个对象,包含value和done两个属性,value自然就是yield后面的东西,也就是产出的结果,而done也好理解,就是这个迭代器是否迭代完成了。

ok,我们继续执行next方法。

const result2 = it.next();
console.log(result2);

执行以上代码后控制台会是怎样的呢?

image.png

是否会有点出乎意料,这次打印出来的result2的done值还是false。

其实也好理解,试问一个函数何时才是真正的结束?很简单,那就是执行到它有返回值为止,也就是执行完return。

在js中,如果没写return,那么默认返回undefined,那么我们就可以将这个Generator函数脑补成以下这样。

function* saga() {
  console.log('start1');
  yield 1;
  console.log('start2');
  yield 2;
  return undefined;
}

而我们执行完第二个next,只是执行完yield 2而已,这个函数还没结束,所以done自然就是false了。

ok,我们继续执行next。

const result = it.next();
console.log(result);

image.png

这下总算是done了,这就表示这个迭代器执行完了。

以上就是Generator函数的基本使用了,还有其他的内容大家可以查mdn,比如迭代器的其他方法如return方法等。这里就不费过多篇章了。

前置知识---events

events是一个事件库,他允许我们自定义事件,然后触发事件后执行目标回调,这里不过多讲述,给大家看看我们需要用的api和简单的用法。

import EventEmitter from "events";

const channel = new EventEmitter();

channel.once("猪头切图仔", () => {
  console.log("猪头切图仔", performance.now());
});

setTimeout(() => {
  channel.emit("猪头切图仔");
}, 1000);

image.png

这里我们定义了'猪头切图仔'事件,然后一秒后触发这个事件,然后就执行了回调也就是打印一些内容了。

redux-saga

有了上述Generator函数基本执行,我们来介绍redux-saga。

在redux-saga中有三个概念:

  • rootSaga: 根saga,也就是唯一入口
  • watcherSaga: 监听saga,监听目标动作,目标动作执行会被监听到
  • workerSaga: 执行saga,上面监听saga监听到目标动作后,通知workerSaga做对应的事

这三个概念其实可以这么理解,假设redux-saga就是一家餐馆,rootSaga其实就是顾客,而watcherSaga就是服务员,workerSaga就是厨师。

顾客(rootSaga)表示自己需要点什么菜,比如肉末茄子,服务员(watcherSaga)监听到服务员需要点肉末茄子,然后去通知workerSaga去做肉末茄子。

这样理解是否就清晰一点了?如果还是不够清晰,那么我们用代码来说话,看一下代码。

我们使用redux和redux-saga建一个仓库。


import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import sagas from "./sagas"; // 这个我们最后写

export const ADD = "ADD";
export const ASYNC_ADD = "ASYNC_ADD";
const initialState = { number: 0 };
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD:
      return { number: state.number + 1 };
    case MINUS:
      return { number: state.number - 1 };
    default:
      return state;
  }
};

const sagaMiddleware = createSagaMiddleware();

export default applyMiddleware(sagaMiddleware)(createStore)(combinedReducer);
sagaMiddleware.run(sagas);

创建一个Counter组件

import { useSelector, useDispatch } from 'react-redux';
import { ADD, ASYNC_ADD } from '@/saga-store';

function App() {
  const counter = useSelector((state) => state.number);
  const dispatch = useDispatch();

  return (
    <div>
      <div>{counter}</div>
      <button onClick={() => dispatch({ type: ADD })}>+</button>
      <button onClick={() => dispatch({ type: ASYNC_ADD })}>+</button>
    </div>
  )
}

export default App

使用react-redux包一下应用入口

import { Provider } from 'react-redux';
import store from './saga-store'; // 上面新建的仓库
import Counter from './components/counter-saga'; // 上面创建的Counter组件

export default function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

好了,这些步骤都完成了,我们开始写saga的代码。

import { take, put } from "redux-saga/effects";
import { ADD, ASYNC_ADD } from "./saga-store";

function delay(delay) {
  return new Promise((resolve) => setTimeout(resolve, delay));
}

function* workerSaga() {
  yield delay(1000);
  // put其实就是调用redux的dispatch
  yield put({ type: ADD });
}

// ps 所谓的effect就是一个简单的对象,这个对象包含了一些middleware解释执行的信息
function* watcherSage() {
  // taker就是产出一个effect,等待有人向仓库派发一个ASYNC_ADD的动作
  // 如果等不到,saga就会暂时停在这里,如果等到了就会继续向下执行
  yield take(ASYNC_ADD);
  yield workerSaga();
}

function* rootSaga() {
  yield watcherSage();
}

export default rootSaga;

上面saga中的代码的take和put现在不必太过关心,后面会实现它他们的。

ok,完成了上面的代码,我们这个应用就能跑起来了。

动画.gif

效果实现了,一秒后加一。

结合上面的代码和这个效果,我们可以猜到它的执行过程了,所谓的createSagaMiddleware产出的middleware可以理解为以一定的逻辑不断执行迭代器的next方法,而saga就是提供这样的一个迭代器。

像这个例子,我们的saga提供了迭代器,middleware获取迭代器然后不断执行next,然后停留在watcherSaga的take这里,等待触发ASYNC_ADD这个动作,当我们点击按钮表示要派发ASYNC_ADD动作后,watcherSaga监听到了,然后有继续不断的执行next,直到结束。

实现

ok,我们经过一定的猜测,了解了大概的执行流程,那么我们可以尝试的去实现它。

先写下一个最简单的redux中间件

function createSgaMiddleware() {
  function sagaMiddleware() {
    return function (next) {
      return function (action) {
        const result = next(action)
        
        return result;
      }
    }
  }
  return sagaMiddleware;
}

export default createSgaMiddleware;

以上是中间件的固定写法,有疑惑的可以翻翻上两篇笔记。

我们回顾到之前的仓库代码,发现这样一行代码sagaMiddleware.run(sagas);,也就是说这个中间件需要有一个run方法。

并且我们的watcherSaga有监听功能,这个功能其实是用events实现的。

结合上面两点,这个createSgaMiddleware改造如下。

import EventEmitter from "events";
import runSaga from "./runSaga";

function createSagaMiddleware() {
  const channel = new EventEmitter();
  let boundRunSaga;
  function sagaMiddleWare({ getState, dispatch }) {
    // 将redux的getState、dispatch和event的channel传下去
    boundRunSaga = runSaga.bind(null, { getState, dispatch, channel });
    return function (next) {
      return function (action) {
        const result = next(action);
        // 当有人要派发动作的时候就会触发事件,至于事件回调是什么,我们在runSaga中揭晓
        channel.emit(action.type, action);
        return result;
      };
    };
  }

  sagaMiddleWare.run = (saga) => boundRunSaga(saga);
  return sagaMiddleWare;
}

export default createSagaMiddleware;

然后我们需要去完成runSaga方法。

const TAKE = "TAKE";
const PUT = "PUT";

// 定义take方法,返回一个effect,这个effect就是个描述,表示是take
export function take(actionType) {
  return { type: TAKE, actionType };
}

// 定义out方法,返回一个effect,这个effect就是个描述,表示是PUT
export function put(action) {
  return { type: PUT, action };
}

export function runSaga(env, saga) {
  // 上面bind的时候传过来的dispatch和channel
  const { dispatch, channel } = env;
  // 这里之所以要判断,主要是下面的next方法会调用runSaga并且传入的saga已经是一个迭代器了
  const it = typeof saga == "function" ? saga() : saga;

  function next() {
    const { done, value: effect } = it.next();

    if (!done) {
      // 这里判断saga中yield的是否为一个迭代器,如例子中的yield watcherSage()和yield workerSaga()都是属于yield了迭代器回来
      if (typeof effect[Symbol.iterator] == "function") {
        runSaga(env, effect);
        next();
      // 判断是否为promise,如例子中的delay
      } else if (effect instanceof Promise) {
        effect.then(next);
      } else {
        // 判断是否触发了定义好的effect,如put和take
        switch (effect.type) {
          case TAKE:
            // 自定义一个事件,等待触发,在例子中,这里就是自定义了ASYNC_ADD事件
            channel.once(effect.actionType, next);
            break;
          case PUT:
            // put方法就是直接执行redux的dispatch
            dispatch(effect.action);
            next();
            break;
          default:
            next();
            break;
        }
      }
    }
  }
  next();
}

不难看出,这个runSaga就是那个不断帮我们执行nect方法的函数,它本质是一个递归,除非遇到特殊情况才会停住,如take方法。

完成了这些代码后,我们自己的redux-saga其实就完成了最基本的雏形了,我们的这个例子其实就已经能跑通了。

结尾

那么,到此为止,我们其实就已经掌握了redux-saga的基本原理了,至于它其他更丰富的功能,其实就是在不断的完善类似take这种方法以及不断完善runSaga而已。

不过,本文留了一个部分没去完成,大家可以根据自己的理解,完善这部分。

  • 需完善部分:

我们回看本文的例子,当我们多点几次这个按钮,它都不再执行一秒后+1这个操作了。

动画.gif

如图,后面无论怎么点都无效。

这原因其实很简单,那就是我们的saga的问题,我们的saga只take了一次,也就是只跟厨师说做一次肉末茄子而已。

如果我们改成两次:

function* watcherSage() {
  yield take(types.ASYNC_ADD);
  yield workerSaga();
  yield take(types.ASYNC_ADD);
  yield workerSaga();
}

动画.gif

诶,你看,是不是点两次也能生效了,但后续又不生效了。很明显,这肯定不合理,redux-saga自然也提供了对应的api解决这个场景下的问题---takeEvery

我们的watcherSage改成takeEvery即可。

function* watcherSage() {
  yield takeEvery(types.ASYNC_ADD, workerSaga);
}

动画.gif

如此,无论我们点多少次都没问题了。

至于takeEvery的实现,就交给各位了。当然,本文最后贴出来的仓库中也会包含这个api的实现的。

那么,本文到此就结束了,希望本文能帮助到各位,另外,觉得本文不错的话,请不要吝啬手中的赞哦🌹🌹🌹

点击查看完整代码