redux相关的笔记之一
前言
近来无事,翻了下曾经的笔记,发现两年前探索redux时的一些笔记,虽说两年时间之久,redux早已迭代多个版本,也废弃了一些api,但笔记并不是各种api如何使用等入门内容,而是redux相关的实现,所以尽管两年过去,再看也仍有收获。
笔记内容很多,重新整理了一下,打算分三篇文章(redux -> react-redux -> redux-saga),本文为第一篇,聊的是redux。
redux
本文节奏为从使用到实现,既如此,我们先来看看最终的例子。
最终例子展示
我们需要实现两个计数器(一号和二号),两个计数器都有简单加和减的功能,且每次变更时都会打印新的和旧的状态。另外,一号计数器的加和减皆为异步执行。所以这里就将一号计数器命名为AsyncCounter,二号计数器命名为Counter。
效果如图:
createStore
最终例子的功能和内容都显得有些复杂了,我们循序渐进,从简单的开始,先从实现计数器2也就是Counter开始。
Counter非常简单,就是一个同步的计数器,仅仅是使用redux的createStore即可。
先看代码,其中路径中的@为src:
- 先初始化一个store
// scr/store/actionTypes.js
export const ADD = "ADD";
export const MINUS = "MINUS";
// 文件:scr/store/reducers/counter.js
import { ADD, MINUS } from '@/store/actionTypes';
function add() { return { type: ADD } }
function minus() { return { type: MINUS } }
const actionsCreators = { add, minus };
export default actionsCreators;
// 文件:scr/store/index.js
import { createStore } from "redux";
import counterReducer from "./reducers/counter";
export default createStore(counterReducer);
- Counter组件
import { useState, useEffect } from 'react';
import store from '@/store';
import { ADD, MINUS } from '@/store/actionTypes';
function Counter() {
const forceUpdate = useState({})[1];
useEffect(() => {
const unsubscribe = store.subscribe(() => {
forceUpdate({});
});
return () => {
unsubscribe();
}
}, []);
const number = store.getState().number;
const add = () => {
store.dispatch({ type: ADD });
}
const minus = () => {
store.dispatch({ type: MINUS });
}
return (
<div>
<div>{number}</div>
<button onClick={add}>+</button>
<button onClick={minus}>-</button>
</div>
)
}
export default Counter;
ok,以上代码就能够使我们的Counter执行起来了。
从以上代码中,我们可以看出核心为createStore方法,而想要了解createStore,我们得从它的使用下手,在store入口文件我们返回的是createStore的产物,我们在Counter组件中使用这个返回的产物,主要用到了三个方法,分别为:
- subscribe:用于订阅, 当状态改变后执行订阅的方法,在本例子中订阅的方法为更新视图。
- dispatch:用于派发一个动作,简而言之为执行一个指定动作去改变状态。
- getState:用于获取store里的状态。
从这三个方法来看,很明显的发布订阅的味道,那么createStore这个方法其实就是实现一个发布与订阅而已。
实现发布与订阅很简单,使用闭包的特性,保存仓库的状态以及订阅的方法(也称为监听器)。然后在派发(dispatch)动作执行完了之后,执行那些之前保存的监听器。
思路很简单,我们直接看代码:
function createStore(reducer) {
let state;
const listeners = [];
function subscribe(listener) {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
function dispatch(action) {
state = reducer(state, action);
listeners.forEach((listener) => {
listener();
});
}
function getState() {
return state;
}
dispatch({ type: "@@redux/init" });
return {
subscribe,
dispatch,
getState,
};
}
export default createStore;
以上代码注意第23行,我们在createStore的时候先执行了一次dispatch,这里的用意主要是为了保存各个reducer的初始值,而且由于是createStore阶段,并没有任何监听器,所以这里不会导致任何的副作用,仅是完成状态的初始化而已。
那么createStore也就实现了,接着我们把原来代码中的createStore改为自己写的这个,Counter是能完美运行的。
applyMiddleware
有了上面实现的createStore,我们来尝试着实现AsyncCounter组件。
- store改造
增加两个异步的actionTypes
// scr/store/actionTypes.js
export const ASYNC_ADD = "ASYNC_ADD";
export const ASYNC_MINUS = "ASYNC_MINUS";
增加asyncCounter的reducer
// 文件:scr/store/reducers/asyncCounter.js
import { ASYNC_ADD, ASYNC_MINUS } from "../actionTypes";
const initialState = { number: 0 };
const reducer = (state = initialState, action) => {
switch (action.type) {
case ASYNC_ADD:
return { number: state.number + 1 };
case ASYNC_MINUS:
return { number: state.number - 1 };
default:
return state;
}
};
export default reducer;
将store应用的reducer改为asyncCounter的reducer
// 文件:scr/store/index.js
import { createStore } from "@/redux"; // @/redux为自己实现的redux
import asyncCounterReducer from "./reducers/asyncCounter";
export default createStore(asyncCounterReducer);
- AsyncCounter组件
import { useState, useEffect } from 'react';
import store from '@/store';
import { ASYNC_ADD, ASYNC_MINUS } from '@/store/actionTypes';
function AsyncCounter() {
const forceUpdate = useState({})[1];
useEffect(() => {
const unsubscribe = store.subscribe(() => {
forceUpdate({});
});
return () => {
unsubscribe();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const number = store.getState().asyncCounter.number;
const asyncAdd = () => {
setTimeout(() => store.dispatch({ type: ASYNC_ADD }), 1000);
}
const asyncMinus = () => {
setTimeout(() => store.dispatch({ type: ASYNC_MINUS }), 1000);
}
return (
<div>
<div>{number}</div>
<button onClick={asyncAdd}>+</button>
<button onClick={asyncMinus}>-</button>
</div>
)
}
export default AsyncCounter;
这样下来,我们好像实现了这个异步计数器,但其实这违背了我们的初衷,我们这个action称为AsyncAdd或者AsyncMinus,但却是通过组件自己实现的异步然后派发一个同步,并非派发了一个异步动作。
所以AsyncCounter组件的asyncAdd和asyncMinus都应该改为:
const asyncAdd = () => {
store.dispatch({ type: ASYNC_ADD });
}
const asyncMinus = () => {
store.dispatch({ type: ASYNC_MINUS });
}
但,如此改了之后,异步就丢失了,我们应该去改reducer吗?
并不,实际上我们需要请redux的中间件机制出山,也就是applyMiddleware。
redux的中间件算是个人比较不喜欢的部分,感觉挺繁琐的,所以一直使用的mobx,这是题外话了。
回归正题,redux中间件的格式是固定的,这里我们以实现这个异步中间件为例。
function thunk() {
return function (next) {
return function (action) {
if (action.type.startsWith("ASYNC")) {
setTimeout(() => {
next(action);
}, 1000);
} else {
next(action);
}
return action;
};
};
}
这个thunk即为一个中间件,它的格式也如代码这样,本身为一个函数,需要返回一个函数,且返回的函数中还需返回一个函数,至于最后是否返回action其实问题不大,
这种格式挺繁琐的,但其实每一个函数的返回都是有其作用的,为了方便描述,我们将这三个嵌套的函数分别命名为一、二、三级函数吧。
最外面那层也就是一级函数,它是有参数的,他的参数为store的getState方法和一个类似store的dispatch方法。我们可以在这个中间件任何地方获得当前仓库状态以及派发动作。
二级函数接收的参数为next,这个next的意思可以理解为执行下一个中间件,因为redux是允许多个中间件合作的,只不过这些中间件是串行工作的。
三级函数接收的参数就是派发的动作了。
了解了这些,我们再写一个中间件,这个中间件的功能则为打印变化前后的状态。
function logger({ getState }) {
return function (next) {
return function (action) {
console.log("老状态", getState());
next(action);
console.log("新状态", getState());
return action;
};
};
}
ok,两个中间件都完成了,我们使用applyMiddleware将他们串起来工作。
// 文件:scr/store/index.js
import { applyMiddleware } from "redux";
import { createStore } from "@/redux"; // @/redux为自己实现的redux
import asyncCounterReducer from "./reducers/asyncCounter";
export default applyMiddleware(thunk, logger)(createStore)(asyncCounterReducer);
如图,中间件就都工作起来了。
整个applyMiddleware的使用,既繁琐又神奇,我们仅是将中间件传给applyMiddleware,它就能让中间件们串行地工作起来。
而这个applyMiddleware的实现的难点也就在于将中间件们串起来。
直接实现applyMiddleware比较难,我们从一个简化的applyMiddleware的题目开始。
我们继续依据中间件的格式,写下如下三个中间件:
const promise = () => (next) => (action) => {
new Promise((resolve) => {
setTimeout(() => {
console.log("promise");
resolve();
}, 500);
}).then(() => {
next(action);
});
};
const thunk = () => (next) => (action) => {
setTimeout(() => {
console.log("thunk");
next(action);
}, 1000);
};
const logger = () => (next) => (action) => {
console.log("logger");
next(action);
};
然后我们派发一个动作:
const dispatch = () => {
console.log("dispatch action");
};
现在要求实现一个compose函数,然后他们按照 半秒后输出promise -> 再一秒后输出thunk -> 输出logger -> 输出dispatch的顺序完成执行。
const middlewares = [promise, thunk, logger].map((middleware) => middleware());
const composed = compose(...middlewares);
const dispatch = () => {
console.log("dispatch action");
};
composed(dispatch)();
这道题,我们重点观察三个中间件以及最后的输出要求,发现所谓的串行,其实每个中间件调用了next方法才去执行下一个中间件,那这个compose就很简单了。
function compose(...args) {
return function (arg) {
for (let i = args.length - 1; i >= 0; i--) {
arg = args[i](arg);
}
return arg;
};
}
几行代码即可搞定,如果想代码行数更少一点,可以像redux源码一样,采用reduce方法,不过我认为for循环会更好理解一些,这里变采用for循环的方式来实现。
最后执行一下:
ok,这个compose实现了,那么applyMiddleware就变的很简单了,注意一下中间件各级函数的入参即可,这里就直接贴代码了。
function applyMiddleware(...middlewares) {
return function (createStore) {
return function (reducer) {
const store = createStore(reducer);
let dispatch;
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action),
};
// 这个compose就是上面实现的compose
const chain = compose(
...middlewares.map((middleware) => middleware(middlewareAPI))
);
dispatch = chain(store.dispatch); // 将被串起来的中间与原始dispatch再串一下
return {
...store,
dispatch, // 替换掉原始的dispatch,保证每一次dispatch都会先走完中间件的逻辑
};
};
};
}
export default applyMiddleware;
注意第5行和第14行,第5行定义一个空的dispath,那是能给一级函数传入一个改写后的dispatch,而非原始的store的dispatch,因为改写后的dispatch才是包含了被串起来的中间件们。而我们在AsyncCounter组件中调用的dispatch也就是这个改写后的dispatch。
然后我们将redux的applyMiddleware改为自己写的applyMiddleware,AsyncCounter能完美运行起来。
combineReducers
为了更好的维护,我们会将每一个不同的reducer分开写,也如本文一样,现在有了两个reducer了,分别是counter的reducer和asyncCounter的reducer,但createStore却只接受一个reducer,为了解决这个问题,redux给出了combineReducers方法,正如其名,用于合并reducer。
- 合并reducer
// 文件:scr/store/reducers/index.js
import { combineReducers } from "redux";
import asyncCounter from "./asyncCounter";
import counter from "./counter";
export default combineReducers({
asyncCounter,
counter,
});
// 文件:scr/store/index.js
import combinedReducer from "./reducers";
export default applyMiddleware(thunk, logger)(createStore)(combinedReducer);
- Counter组件以及AsyncCounter组件 将两个计数器组件的number取值改一改即可
// Counter组件
const number = store.getState().counter.number;
// AsyncCounter组件
const number = store.getState().asyncCounter.number;
完成这些改动后,跑起来就是本文一开始的最终例子的效果了。
接下来,我们继续从使用处下手来理解combineReducers方法。
从以上改动可以看出,我们仅是在状态的取值方面有所变化,派发动作是没有变化的,那么不难猜出combineReducers方法其实就是将状态依据reducer的命名来分开保存,也就是在各个状态外面套多了一层。
将counter的{ number: 0 }
和asyncCounter的{ number: 0 }
合并成:
{
counter: { number: 0 },
asyncCounter: { number: 0 },
}
那么combineReducers方法的实现就很简单了:
function combineReducers(reducers) {
return function combination(state = {}, action) {
const nextState = {};
for (let key in reducers) {
const prevStateForKey = state[key];
const reducerForKey = reducers[key];
const nextStateForKey = reducerForKey(prevStateForKey, action);
nextState[key] = nextStateForKey;
}
return nextState;
};
}
export default combineReducers;
最后,我们把redux的combineReducers改成自己的combineReducers,两个计数器组件也能完美运行起来。
结尾
ok,到此,redux核心的三个方法也就实现了,我们的最终例子也完美运行了起来,而完整代码则在本文最后。那么本文所有内容到此就结束了,本文通过最终需要实现的例子为引,一步一步揭开redux三个核心api---createStore、applyMiddleware以及combineReducers的神秘面纱,并实现了他们,以此了解redux基本工作机制与原理。
最后,希望这篇文章能对大家有所帮助,另外,觉得本文不错的话,请不要吝啬手中的赞哦🌹🌹🌹
转载自:https://juejin.cn/post/7365757742381940777