前言
在React,Vue,Angular等声明UI库问世之后;有人曾经说过,前端正在从由DOM驱动UI生成 转变为 由状态驱动UI生成,并通过前端状态机中状态的变化,控制页面上展示不同的UI以及驱动页面上的动态脚本
Redux就是这个由状态驱动UI更新发展过程中催生的产物之一
状态管理
我们一般所说的状态
无非就是由React - state 控制并管理的状态变量
或者是由vue - reactive | ref 控制的响应式状态
那么随着状态管理的日趋复杂和业务化,我们需要针对统一的状态管理做场景区分(以提升状态管理的可拓展和可维护),状态机应运而生
常见的状态机
常见的状态机有 Redux, Vuex, Mobx, Recoil, Zustand, 以及用Rxjs打造的状态库
那状态库中的数据保存在哪里呢?怎么保存呢?
-
状态保存最最核心的是:不被GC(能够长时间存储在内存中)
-
保存方式:
- Window / Global
- Closure 闭包
状态管理如何实现?
- 在组件之外,可以全局共享数据状态 (闭包)
- 修改状态之后,使用状态的相关方要能感知 (响应式数据 - mobx, 发布订阅 - redux)
- 修改状态之后,会触发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);