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 的位置,回到页面可以看到效果和刚才一模一样。