前言
本文是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,此时控制台的输出会是什么?
是的,输出的是start1和result1,我们调用了next,它执行完yield获取到了产出就停止了,需要继续执行next方法才会继续。
然后我们关注一下result1的数据形式,它是一个对象,包含value和done两个属性,value自然就是yield后面的东西,也就是产出的结果,而done也好理解,就是这个迭代器是否迭代完成了。
ok,我们继续执行next方法。
const result2 = it.next();
console.log(result2);
执行以上代码后控制台会是怎样的呢?
是否会有点出乎意料,这次打印出来的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);
这下总算是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);
这里我们定义了'猪头切图仔'
事件,然后一秒后触发这个事件,然后就执行了回调也就是打印一些内容了。
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,完成了上面的代码,我们这个应用就能跑起来了。
效果实现了,一秒后加一。
结合上面的代码和这个效果,我们可以猜到它的执行过程了,所谓的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这个操作了。
如图,后面无论怎么点都无效。
这原因其实很简单,那就是我们的saga的问题,我们的saga只take了一次,也就是只跟厨师说做一次肉末茄子而已。
如果我们改成两次:
function* watcherSage() {
yield take(types.ASYNC_ADD);
yield workerSaga();
yield take(types.ASYNC_ADD);
yield workerSaga();
}
诶,你看,是不是点两次也能生效了,但后续又不生效了。很明显,这肯定不合理,redux-saga自然也提供了对应的api解决这个场景下的问题---takeEvery
我们的watcherSage改成takeEvery即可。
function* watcherSage() {
yield takeEvery(types.ASYNC_ADD, workerSaga);
}
如此,无论我们点多少次都没问题了。
至于takeEvery的实现,就交给各位了。当然,本文最后贴出来的仓库中也会包含这个api的实现的。
那么,本文到此就结束了,希望本文能帮助到各位,另外,觉得本文不错的话,请不要吝啬手中的赞哦🌹🌹🌹