Redux 是一个非常不错的状态管理库,和 Vuex 不同的是 Redux 并不和 React 强绑定,你甚至可以在 Vue 中使用 Redux。当初的目标是创建一个状态管理库,来提供最简化 API。
复习
具体 Redux 的使用细节我们不再啰嗦,网上有很多教程,这里只使用Redux 最基本的用法。
首先实现 store
import { Action, legacy_createStore as createStore } from "redux";
const reducer = (state = 0, action: Action) => {
switch (action.type) {
case 'ADD':
return state + 1
case 'SUB':
return state - 1
default:
return state
}
}
const store = createStore(reducer)
export default store
createStore 已经被标记为废弃,但是提供了一个 legacy_createStore 方法用于兼容之前的代码,相较之下官方更推荐使用 RTK
然后在页面中使用store,这里我们先复习一下 store 对象的几个常用 API
- getState:获取 store 的当前状态;
- dispatch:传递一个 Action 对象作为参数,用于修改状态;
- subscribe:订阅,用于在状态发生改变时执行传入的回调(例如更新渲染),返回一个函数,用于取消订阅。
import store from './store'
class Count extends Component {
unsubscribe: (() => void) | undefined;
componentDidMount() {
// 组件挂载时订阅 store
this.unsubscribe = store.subscribe(() => {
this.forceUpdate()
})
}
componentWillUnmount() {
// 卸载组件时取消订阅
this.unsubscribe!()
}
add = () => {
store.dispatch({ type: 'ADD' })
}
sub = () => {
store.dispatch({ type: 'SUB' })
}
render() {
return <div>
{store.getState()}
<button onClick={this.add}>+</button>
<button onClick={this.sub}>-</button>
</div>
}
}
现在我们的页面上就可以看到效果了,点击对应的按钮数据就会发生相应的改变
实现一个丐版
我们基于上面的复习代码作为测试用例,我们来手动实现这几个常用的 API。首先在 src 下创建目录 /src/lib/redux
,新建 index.ts 文件
export function legacy_createStore
<S, A extends Action>(reducer: Reducer<S, A>) {
let currentState: S;
let listeners: Array<() => void> = [];
function getState() {
}
function dispatch<T extends A>(action: T) {
}
function subscribe(listener: () => void) {
}
return {
getState,
dispatch,
subscribe
}
}
getState 很简单,只需要把 currentState 返回即可;subscribe 需要将传入的回调函数保存到 linsteners 中,然后把移除回调的函数返回即可。
function getState() {
return currentState
}
function subscribe(listener: () => void) {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
}
}
在处理 dispatch 的时候,要注意 dispatch 是有返回值的,返回值是传入的 action,我们跟随 redux 一样实现即可。
function dispatch<T extends A>(action: T) {
currentState = reducer(currentState, action);
listeners.forEach(listener => listener())
return action;
}
此时三个常用的 API 已经完成,此时将组件中引用的 redux 替换为我们刚才在本地实现的 redux,打开浏览器会看到原来显示的 0 并没有成功想显示,但是点击两个按钮对应的值却正确的显示了出来。
这是因为我们刚才的逻辑中没有给 currentState 赋初始值,我们只需要在刚才的函数中触发一次 dispatch 即可,这里传入的 action.type 写一个用户不会写的值集即可,例如当前的日期。
dispatch({ type: new Date().toUTCString() })
此时页面实现的效果就跟我们之前用官方redux 实现的效果一样了。
增强:中间件
目前为止 redux 的 dispatch 只支持最基础的对象作为 action,如果你想使用函数作为 action,会在控制台看到报错
sub = () => {
// @ts-expect-error
store.dispatch(dispatch => {
setTimeout(() => {
dispatch({type: "SUB"});
}, 1000);
});
}
这时候 redux 会提示你是否需要为 redux 安装中间件,这里我们为 redux 安装 thunk 和 logger 两个中间件
$ npm install redux-thunk redux-logger
由于 redux-logger 本身没有声明类型,所以在使用ts 时会抛出错误,我们需要额外为 logger 安装类型包
$ npm install @types/redux-logger -D
安装完类型包之后我们需要在 tsconfig.json 中添加类型,否则编译时还是无法解析类型
{
"compilerOptions": {
"types": [
"redux-logger"
]
},
}
此时我们就可以为 redux 添加中间件了
import { Action, applyMiddleware } from "redux";
import { legacy_createStore as createStore } from "redux";
import thunk from "redux-thunk";
import logger from 'redux-logger';
const reducer = (state = 0, action: Action) => {
switch (action.type) {
case 'ADD':
return state + 1
case 'SUB':
return state - 1
default:
return state
}
}
const store = createStore(reducer, applyMiddleware(thunk, logger))
export default store
此时将我们在页面中点击按钮,不会再抛出异常
在了解了中间件的使用方法之后,我们也给我们的丐版 redux 添加中间件机制。
上一小节的 Reducer 其实是函数式编程概念中的纯函数,函数式编程中还有另一个概念——柯里化,这里我们就不再强调,不了解的可以百度。
先来看和一个示例
function f1(arg: any) {
console.log('f1', arg);
return arg;
}
function f2(arg: any) {
console.log('f2', arg);
return arg;
}
function f3(arg: any) {
console.log('f3', arg);
return arg;
}
我们想让f1函数的返回值作为f2的参数,再让 f2 的返回值作为 f3的参数,按照最简单的方法,我们可以嵌套调用
f3(f2(f1('function')))
这样虽然可以实现我们要的效果,但是如果函数多了的时候就很不方便,我们可以利用柯里化,让函数自动完成嵌套
const compose = (...fns: Function[]) => {
if (fns.length === 0) {
return (arg: any) => arg;
}
if (fns.length === 1) {
return fns[0];
}
return fns.reduce(
(fn1, fn2) =>
(...args: any[]) =>
fn1(fn2(...args)),
);
};
compose(f1, f2, f3)('function')
// f3 function
// f2 function
// f1 function
我们利用数组的 reduce 方法,将函数依次进行嵌套,这其实就是 redux 中 middleware 的实现方式。
现在有没有感觉很眼熟,我们刚才引入的 thunk 和 logger其实就是一个函数,我们引入中间件的方式就跟这里调用 compose 类似。
我们来自己实现一个 applyMiddleware,我们需要改写原有的 dispatch,让其可以通过 middleware 来增强功能。
export function applyMiddleware(...middlewares: Middleware[]) {
return (createStore: typeof legacy_createStore) => (reducer: Reducer) => {
const store = createStore(reducer)
let dispatch = store.dispatch
const midAPI = {
getState: store.getState,
dispatch: (action: Action, ...args: any[]) => dispatch(action, ...args)
}
// 给 middleware 传入新的 dispatch
const middlewareChain = middlewares
.map(middleware => middleware(midAPI))
// 增强 dispatch(把所有中间件函数都执行)
dispatch = compose(...middlewareChain)(store.dispatch)
return {
...store,
dispatch
}
}
};
这时替换掉import 的位置,回到页面可以看到效果和刚才一模一样。