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>
  }
}

现在我们的页面上就可以看到效果了,点击对应的按钮数据就会发生相应的改变

image-20220830102202475

实现一个丐版

我们基于上面的复习代码作为测试用例,我们来手动实现这几个常用的 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 一样实现即可。

image-20220830155931788

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);
  });
}

image-20220830202006074

这时候 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

此时将我们在页面中点击按钮,不会再抛出异常

image-20220830202705515

在了解了中间件的使用方法之后,我们也给我们的丐版 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 的位置,回到页面可以看到效果和刚才一模一样。


前端小白