开始
手动实现一个状态管理,有哪些关注点
- 状态存储独立于组件之外(全局变量,闭包)
- 状态变更后,相关方能够感知到(发布订阅模式)
- 状态变更后,能够触发 UI 更新(setState)
v1 版本
先使用发布订阅模式实现一个简易版,代码如下,Show 只负责展示 count,所以需要订阅 count 的变更,Operate 负责发布 count 的变更
// App.jsx
import { useEffect, useState, useRef } from "react";
import store from "./v1";
const App = () => {
return (
<div>
<Show />
<hr />
<Operate />
</div>
);
};
const Show = () => {
const [v, setV] = useState(false);
const { count } = store.get();
const { subscribe } = store;
const update = () => {
setV((v) => !v);
};
useEffect(() => {
subscribe(update);
return () => {
unsubscribe(update);
};
}, []);
return (
<>
<div>Show</div>
<div>{count}</div>
</>
);
};
const Operate = () => {
const count = useRef();
const fetchCount = () => {
count.current = store.get().count;
};
fetchCount();
const { changeData } = store;
const handleClick = () => {
const res = count.current + 1;
changeData({
count: res,
});
fetchCount();
};
return (
<>
<div>Operate</div>
<button onClick={handleClick}>handleClick</button>
</>
);
};
export default App;
下面是 store 的实现,核心就是发布订阅模式
const createStore = (initState) => {
let data = initState;
const deps = [];
const get = () => {
return data;
};
const subscribe = (cb) => {
deps.push(cb);
};
const unsubscribe = (dep) => {
const index = deps.findIndex((v) => v === dep);
deps.splice(index, 1);
};
const _notify = (val) => {
deps.forEach((cb) => cb(val));
};
const changeData = (val) => {
data = val;
_notify(val);
};
return { get, subscribe, changeData, unsubscribe };
};
const initState = {
count: 1,
};
export default createStore(initState);
虽然可以满足需求,但是有几个问题
1、心智负担较重,接入方需要手动订阅变更
2、接入方来变更状态,可能会导致异常,比如 { count: 0 } 更新为 { count: { count: 0 }} ,导致结果不可预测,不可控
v2 版本
借鉴 reducer 的设计理念,准备一些纯函数,使用 state 和 action 来生成新的状态,外界只能通过提交不同的 action 来实现状态变更,将变更状态的操作下沉到 store 内部,如下图所示
暂时无法在飞书文档外展示此内容
有两个比较大的变更点
-
需要准备 reducer 用来变更状态,而且接入方也要修改调用方式
-
状态变更后,需要更新界面,这里会实现一个简单的 connect 组件
书写 redcuer
引入 reducer 之后,规范了变更状态的行为,并且还可以实现 payload 的校验,对两个属性分别实现一个 reducer,保证状态隔离,并且还需要一个 combineReducer 来合并 reducer
const countReducer = (state, action) => {
switch (action.type) {
case "ADD_COUNT": {
return {
...state,
count: state.count + 1,
};
}
case "CHANGE_COUNT": {
if (typeof action.payload !== "number") {
throw new Error("count must be a number");
}
return {
...state,
count: action.payload,
};
}
default: {
return state;
}
}
};
const ageReducer = (state, action) => {
switch (action.type) {
case "ADD_AGE": {
return {
...state,
age: state.age + 1,
};
}
case "CHANGE_AGE": {
if (typeof action.payload !== "number") {
throw new Error("age must be a number");
}
return {
...state,
age: action.payload,
};
}
default: {
return state;
}
}
};
const combineReducer = (reducers) => (state, action) => {
let finalState = {};
Object.keys(reducers).forEach((key) => {
const reducer = reducers[key];
const { [key]: res } = reducer(state, action);
finalState[key] = res;
});
return finalState;
};
const createStore = (reducer, initState) => {
let data = initState;
const deps = [];
const get = () => {
return data;
};
const subscribe = (dep) => {
deps.push(dep);
};
const _notify = () => {
deps.forEach((dep) => dep(data));
};
const _set = (newState) => {
data = newState;
};
const dispatch = (action) => {
const state = get();
const finalState = reducer(state, action);
_set(finalState);
_notify();
};
return { get, dispatch, subscribe };
};
const initState = {
count: 1,
age: 18,
};
const reducer = combineReducer({
count: countReducer,
age: ageReducer,
});
export default createStore(reducer, initState);
使用时也很清爽,不需要任何 useAPI,修改时只需要 dispatch 即可
// App.js
import store from './v1'
const App = () => {
return (
<div>
<Show />
<hr />
<Operate />
</div>
);
};
const Show = () => {
const { count, age } = store.get()
return (
<>
<div>Show</div>
<div>{count}</div>
<div>{age}</div>
</>
);
};
const Operate = () => {
const addCount = () => {
dispatch({
type: "ADD_COUNT",
});
}
const assignCount = () => {
dispatch({
type: "CHANGE_COUNT",
payload: 10,
});
}
const addAge = () => {
dispatch({
type: "ADD_AGE",
});
}
const assignAge = () => {
dispatch({
type: "CHANGE_AGE",
payload: 30,
});
}
return (
<>
<div>Operate</div>
<button onClick={addCount}>count++</button>
<button onClick={assignCount}>count = 10</button>
<button onClick={addAge}>age++</button>
<button onClick={assignAge}>age = 30</button>
</>
);
};
export default App;
运行后发现,虽然 count 的值被改变了,但是页面并没有更新,因为并没有通知 react,更新界面,必须调用 setState,能不能把这个事情变成接入层无感知的呢
实现 connect
可以尝试一个思路,在最外层建立一个组件,在其内部完成 setState,由于 react 默认会更新其后代,可以利用这个特性来实现接入层无感知的更新,先来看看 connect 的用法,从参数可知,这个组件可以让接入层通过 props 来接收 state 和 dispatch
connect(mapStateToProps, mapDispatchToProps)(App)
主要功能有两个
- 实现 state,disptach 的映射
- 自动更新
使用 useContext 来实现 store 的透传,并且订阅状态的变更,实现界面更新,将 state 和 dispatch 作为 props 进行分发,完整代码如下
// connect.jsx
import { useContext } from "react";
import { useState, useEffect } from "react";
import ReduxContext from "./v1/context";
const connect = (mapStateToProps, mapDispatchToProps) => (Component) => {
return (props) => {
mapStateToProps = mapStateToProps || (() => ({}));
mapDispatchToProps = mapDispatchToProps || (() => ({}));
const [v, setV] = useState(false);
const store = useContext(ReduxContext)
useEffect(() => {
store.subscribe(() => {
setV((v) => !v);
});
}, []);
return (
<Component
{...props}
{...mapStateToProps(store.get())}
{...mapDispatchToProps(store.get(), store.dispatch)}
/>
);
};
};
export default connect;
// App.jsx
import connect from "./connect.jsx";
const App = () => {
return (
<div>
<Show />
<hr />
<Operate />
</div>
);
};
const _Show = ({ count, age }) => {
return (
<>
<div>Show</div>
<div>{count}</div>
<div>{age}</div>
</>
);
};
const mapStateToProps = (state) => ({
count: state.count,
age: state.age,
});
const Show = connect(mapStateToProps)(_Show);
const mapDispatchToProps = (state, dispatch) => {
return {
addCount: () => {
dispatch({
type: "ADD_COUNT",
});
},
assignCount: (count) => {
dispatch({
type: "CHANGE_COUNT",
payload: count,
});
},
addAge: () => {
dispatch({
type: "ADD_AGE",
});
},
assignAge: (age) => {
dispatch({
type: "CHANGE_AGE",
payload: age,
});
},
};
};
const _Operate = ({ addCount, assignCount, addAge, assignAge }) => {
return (
<>
<div>Operate</div>
<button onClick={addCount}>count++</button>
<button onClick={() => assignCount(10)}>count = 10</button>
<button onClick={addAge}>age++</button>
<button onClick={() => assignAge(30)}>age = 30</button>
</>
);
};
const Operate = connect(() => ({}), mapDispatchToProps)(_Operate);
export default App;
// context.js
import { createContext } from "react";
const ReduxContext = createContext({});
export default ReduxContext;
// main.jsx
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import ReduxContext from "./v1/context.js";
import store from "./v1/index.js";
createRoot(document.getElementById("root")).render(
<ReduxContext.Provider value={store}>
<App />
</ReduxContext.Provider>
);