重写一个简单的redux

151 阅读5分钟

前言

在React,Vue,Angular等声明UI库问世之后;有人曾经说过,前端正在从由DOM驱动UI生成 转变为 由状态驱动UI生成,并通过前端状态机中状态的变化,控制页面上展示不同的UI以及驱动页面上的动态脚本

Redux就是这个由状态驱动UI更新发展过程中催生的产物之一

状态管理

我们一般所说的状态

无非就是由React - state 控制并管理的状态变量

或者是由vue - reactive | ref 控制的响应式状态

那么随着状态管理的日趋复杂和业务化,我们需要针对统一的状态管理做场景区分(以提升状态管理的可拓展和可维护),状态机应运而生

常见的状态机

常见的状态机有 Redux, Vuex, Mobx, Recoil, Zustand, 以及用Rxjs打造的状态库

那状态库中的数据保存在哪里呢?怎么保存呢?

  1. 状态保存最最核心的是:不被GC(能够长时间存储在内存中)

  2. 保存方式:

    • Window / Global
    • Closure 闭包

状态管理如何实现?

  1. 在组件之外,可以全局共享数据状态 (闭包)
  2. 修改状态之后,使用状态的相关方要能感知 (响应式数据 - mobx, 发布订阅 - redux)
  3. 修改状态之后,会触发UI更新 (响应式数据 - 数据修改驱动UI自动更新, 发布订阅 - 感知数据变化后,通过特定动作驱动UI更新)

Redux的实现的基本原理

按照上述所说,Redux感知数据变化和驱动UI更新的前提是使用可支持发布订阅的闭包

根据我们对Redux的使用,进行上述状态管理的实现

第一步:实现 1

第二步:实现 2,3

第一步:实现闭包

  • createStore.js
const createStore = (reducer, initialState) => {
  let state = initialState;
  const listeners = [];  // 监听管理,用于管理所有订阅方法

  // redux 基于dispatch - action的方式修改状态
  const dispatch = (action) => {
    // 通过reducer 纯函数进行状态更新,并返回最新状态
    state = reducer(state, action); 
    
    // 状态更新后,通知相应订阅方(实现发布订阅中的发布)
    listeners.forEach(l => l());
  };

  const subscribe = (fn) => {
    // 发布订阅中的订阅
    listeners.push(fn);
  };

  // 返回最新状态
  const getState = () => state;

  return { dispatch, subscribe, getState };
};

闭包实现

闭包原理:

当一个函数作用域对另外一个函数作用域中的状态(变量,函数等)产生了引用关系(存储在内存中,无法通过GC回收),那么就形成了闭包

这里的getState因为都是使用了createStore函数作用域下的状态(state),因此已经形成了闭包,在全局作用域的任何位置调用这个方法的都能拿到最新的state

实现combineReducer

根据不同业务场景下需要修改不同的state,我们通过combineReducer来对reducer进行组合

  • createStore.js
// 初始化数据
const initialState = {
  counter: {
    count: 0,
  },
  info: {
    name: "xw",
    age: 0,
  },
};

// 修改counter的Reducer
const counterReducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 };
    case "DECREMENT":
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

// 修改info的Reducer
const infoReducer = (state, action) => {
  switch (action.type) {
    case "SET_NAME":
      return { ...state, name: action.payload.name };
    case "SET_AGE":
      return { ...state, age: action.payload.age };
    default:
      return state;
  }
};

// 组合Reducer
const combineReducers = (reducers) => {
  const keys = Object.keys(reducers);

  // 返回一个reducer,并形成对keys的闭包,用来遍历传入combineReducer中的所有reducer
  // 这里的state,action来源于createStore的dispatch函数,因为dispatch对createStore中的state产生了闭包,因此可以拿到更新state之前的最新state
  return (state, action) => {
    const nextState: any = {};

    keys.forEach((key: any) => {
      const reducer = reducers[key];
      const previousState = state[key];

      const next = reducer(previousState, action);
      nextState[key] = next;
    });

    return nextState;
  };
};

const reducers = combineReducers({
  counter: counterReducer,
  info: infoReducer,
});

export const store = createStore(reducers, initialState);

实现store的初始化注入

我们在使用Redux的时候,会在根节点上包裹一个 Context.Provider 并提供初始的value,因此下面我们通过 React.createContext 来实现

  • context.js
import { createContext } from "react";

export const Context = createContext({});
  • index.js - 根文件
import { Context } from "./redux/context";
import { store } from "./redux/createStore";
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);

root.render(
  <Context.Provider value={store}>
    <App />
  </Context.Provider>
);

第二步:实现发布订阅和UI更新

在使用Redux的过程中,我们知道这里需要通过 react-redux 实现对根节点下的所有组件提供最新状态以及更新状态的方法

想一想,假如没有 react-redux,按照现有第一步,如果我们要消费状态机的最新状态和使用更新状态方法的话,是不是每一个组件都必须要用 Context.Consumer 来包裹,并且当数据更新,触发订阅回调之后,需要更新组件自身的状态用以重新渲染组件

react-redux的connect就是帮助我们减轻这个心智负担所推出的HOC(高阶组件

connect基本用法
const MyComponent = () => {
    return (
        <div>...</div>
    )
}

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)
connect的实现
import { useContext, useEffect, useState } from "react";
import { Context } from "./context";

export const connect =
  (mapStateToProps, mapDispatchToProps) => (MyComponent: any) => {
    // 执行 connect(mapStateToProps, mapDispatchToProps)(MyComponent) 后返回一个 全新组件
    return (props) => {
      const _store = useContext(Context);

      const [bool, setBool] = useState(true);
      const forceUpdate = () => setBool((prev) => !prev);  // 数据更新之后,强制重新渲染当前组件

      useEffect(() => {
        _store.subscribe(forceUpdate); // 订阅消息
      }, []);

      return (
        <Context.Consumer>
          {(store) => (
            <MyComponent
              {...props}
              {...mapStateToProps(store.getState())} // 拿到最新状态
              {...mapDispatchToProps(store.dispatch)} // 拿到更新状态的dispatchAction
            />
          )}
        </Context.Consumer>
      );
    };
  };    

试用一下

  • ComponentTest.jsx
import { Button } from "antd";
import { connect } from "./connect";

const ComponentTest = (props) => {
  console.log(props);

  const { counter, info, increment, decrement, setName, setAge } = props;

  return (
    <div>
      <span>count: {counter.count}</span>
      <Button onClick={increment}>增加</Button>
      <Button onClick={decrement}>减少</Button>
      <Button onClick={setName}>设置姓名: {info.name}</Button>
      <Button onClick={setAge}>设置年龄: {info.age}</Button>
    </div>
  );
};

const mapStateToProps = (state) => {
  return {
    counter: state.counter,
    info: state.info,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    increment: () => {
      dispatch({ type: "INCREMENT" });
    },
    decrement: () => {
      dispatch({ type: "DECREMENT" });
    },
    setName: () => {
      dispatch({ type: "SET_NAME", payload: { name: "px" } });
    },
    setAge: () => {
      dispatch({ type: "SET_AGE", payload: { age: 27 } });
    },
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(ComponentTest);

20250105135720.gif