前置知识
首先,我们要知道 React 中能够触发更新的所有方法:
- ReactDom.render: HostRoot
- useState & useReducer: FunctionComponent
- this.setState & this.forceUpdate: ClassComponent
子组件 props
的变更导致的更新其实也是由于父组件的 state
变更从而发生重新 render
。
本文所有的代码实现和最终演示Demo都在这里:codesandbox.io/s/mystifyin…
正文
首先通过一个例子来引出我们这里实现一个状态管理的最基本的思路。
一个最简单的组件 App.tsx
:
import { useRef, useState } from "react";
const App: React.FC = () => {
// useState 会触rerender
const [exe, setExe] = useState<number>(0);
// useRef 的值变更不会触发rerender
const count = useRef<number>(0);
return (
<div>
App
<br />
count: {count.current}
<br />
<button
onClick={() => {
count.current += 1;
setExe(exe + 1);
}}
>
update count
</button>
</div>
);
};
export default App;
页面中的 count
是来自 useRef
保存的值,本来 useRef
的值变更是不会触发组件的重新渲染的,页面中的 count
永远都是 0
,但是我们通过加上一个 state (exe)
(setState
会触发组件重新渲染),然后达到了我们的目的:useRef
的最新值也会随着按钮的点击实时渲染到页面上。这里的状态 exe
就相当于一个触发器了,触发组件更新重新渲染。
所以这时候在我们脑海里是不是出现了一个全局状态管理思路:useRef
相当于保存的全局状态(因为是全局的状态,其他组件也会用到,所以后续我们把状态放在一个全局变量里),然后每次更改状态的值调用一下我们的触发器setExe
,让页面重新更新(其实状态管理的思路就是这么简单,但是其中还是有一些问题和细节需要处理的,后面我们会一一去实现)。
最后,在开始实现自己的全局状态管理之前,先介绍一下我们要实现的是什么样的状态管理,这关乎到实际的实现方式,本文的目标是实现出如下使用方式的状态管理:
// 我们有一个 createStore 方法,去创建我们的状态
function createStore() {
// 待实现...
}
// 调用 createStore 方法创建全局状态,参数为默认状态和默认值
const [useStore] = createStore<{ count: number; name: number }>({
count: 1,
name: 1,
});
// 组件中 我们这么使用
// App.tsx
const App: React.FC = () => {
// 这里的 useStore 接收一个参数 是一个function 这是后面我们要实现的功能
// 主要目的是用于性能优化 比如我们创建了{ count: number; name: number }状态
// 但是我们的组件只用到了count 那么我只需要在count变化时才会触发组件更新
// 其他这个组件不关心的状态变更时我们是不希望组件更新的 后面我们会实现这个功能
const [{ count }, updateStore] = useStore((store) => [store.count]);
return (
<div>
App
<br />
count: {count}
<br />
<button
onClick={() => {
updateStore({ count: count + 1 });
}}
>
update count
</button>
</div>
);
};
// Other.tex
const Other: React.FC = () => {
// 引用 count
const [{ count }] = useStore((store) => [store.count]);
return (
<div>
Other
<br />
count: {count}
</div>
);
}
当 App
中调用 updateStore({ count: count + 1 })
时,App
和 Other
组件都会重新渲染并拿到最新的 count
。
开始实现
按照刚刚的思路我们有了如下的代码。
store.ts
:
import { useEffect, useState, useRef } from "react";
// 用来保存全局的状态
let store: Record<string, any> = {};
export function createStore<T extends object>(
initialVal: T
): [UseStoreType<T>] {
// 初始化 store
store = { ...initialVal };
return [useStore];
}
function useStore() {
const [exe, setExe] = useState<number>(0);
function updateStore(val) {
// 更新 store
store = {
...store,
...val
}
// 调用一次触发器 让组件 rerender
setExe(exe + 1);
}
return [store, updateStore];
}
// 创建store
const [useStore] = createStore<{ count: number }>({
count: 1
});
// App 组件
const App: React.FC = () => {
const [{ count }, updateStore] = useStore();
return (
<div>
App
<br />
count: {count}
<br />
<button
onClick={() => {
updateStore({ count: count + 1 });
}}
>
update count
</button>
</div>
);
};
// Other 组件
const Other: React.FC = () => {
// 引用 count
const [{ count }] = useStore();
// App 组件里面更新count 是不会引起 Other 组件更新
return (
<div>
Other
<br />
// 不会改变
count: {count}
</div>
);
}
但是这个时候 App
组件中调用 updateStore({ count: count + 1 })
只有 App
组件自身的 count
变成最新的了,Other
组件没有任何反应,这是因为,每个组件中调用 const [{ count }] = useStore();
的时候组件自身都创建了一个触发器 const [exe, setExe] = useState<number>(0);
,updateStore
里面调用的触发器都是自身组件的,所以只能让自身组件触发重新渲染。
这时候我们只需要通知到每个组件调用自己的触发器 setExe
就行了:
import { useEffect, useState, useRef } from "react";
// 一个简单的订阅器
class Managger {
constructor(readonly listeners: CallbackType[] = []) {}
subscript(callback: CallbackType) {
this.listeners.push(callback);
}
notyfy(keys: KeysType) {
this.listeners.forEach((callback) => {
callback(keys);
});
}
}
// 发布订阅 用来通知所有组件更新
const manager = new Managger();
// 用来保存全局的状态
let store: Record<string, any> = {};
export function createStore<T extends object>(
initialVal: T
): [UseStoreType<T>] {
// 初始化 store
store = { ...initialVal };
return [useStore];
}
function useStore() {
const [, setExe] = useState<number>(0);
// 避免useEffect中需要引用 state exe
const exe = useRef<number>(0);
// didMount
useEffect(() => {
manager.subscript(() => {
// 调用触发器
setExe(++exe.current);
});
}, []); // 这里就不需要 [exe] 了
function updateStore(val) {
// 更新 store
store = {
...store,
...val
}
// 通知所有订阅者
manager.notyfy();
}
return [store, updateStore];
}
还是刚刚那个例子,这时候 App
中调用更新 count+1
,那么这时候所有使用过 count
的组件都会更新了。
还有一个功能我们还没实现,就是一开始说的 const [{ count }, updateStore] = useStore((store) => [store.count])
支持的参数,用来避免组件不必要的更新。那么继续更新我们的代码:
import { useEffect, useState, useRef } from "react";
type KeysType = string[];
type CallbackType = (keys: KeysType) => void;
type DepFn<T extends object> = (store: T) => unknown[];
type Result<T> = [T, (val: Partial<T>) => void];
type UseStoreType<T extends object> = (depsFn?: DepFn<T>) => Result<T>;
class Managger {
constructor(readonly listeners: CallbackType[] = []) {}
subscript(callback: CallbackType) {
this.listeners.push(callback);
}
// 新增参数 keys 用来对比
notyfy(keys: KeysType) {
this.listeners.forEach((callback) => {
callback(keys);
});
}
}
const manager = new Managger();
let store: Record<string, any> = {};
// (store) => [store.name]
function useStore<T extends object>(depsFn?: DepFn<T>): Result<T> {
// 触发器
const [, setExe] = useState<number>(0);
const exe = useRef<number>(0);
// 记录当前组件使用了哪些 key
const keys = useRef<KeysType>([]);
// 主要变更是这里 通过 Proxy 获取组件使用了哪些 key
const proxy = useRef(
new Proxy<T>(store as T, {
get: function (target, propKey: string, receiver) {
console.log(`getting ${propKey}!`);
keys.current.push(propKey);
return Reflect.get(target, propKey, receiver);
},
})
);
// didMount 只执行一次
useEffect(() => {
// 默认调用一次 通过 Proxy 来获取组件使用了哪些 key
depsFn?.(proxy.current);
manager.subscript((changed: KeysType) => {
// 判断此次变更的状态中是否有组件自身中用到的 key
// 有则调用触发器更新
if (
keys.current.some((key) => changed.includes(key)) ||
!keys.current.length
) {
setExe(++exe.current);
}
});
return () => {
// TODO manager.removeListener();
}
}, []);
function updateStore(val: Partial<T>) {
store = {
...store,
...val,
};
// 通过打印 manager.listeners.length 判断段是否重复添加了listener
console.log(`listener's length: ${manager.listeners.length}`);
manager.notyfy(keys.current);
}
return [store as T, updateStore];
}
export function createStore<T extends object>(
initialVal: T
): [UseStoreType<T>] {
store = { ...initialVal };
return [useStore];
}
最后我们测试一下:
export const [useStore] = createStore<{ count: number; name: number }>({
count: 1,
name: 1
});
// App.tsx
import { useStore } from "./utils";
import { Test } from "./Test";
import { Result } from "./Result";
export default function App() {
const [{ count }, updateStore] = useStore((store) => [store.count]);
// 打印 组件是否更新
console.log("rerender from App");
return (
<div>
App
<br />
count: {count}
<br />
<button
onClick={() => {
updateStore({ count: count + 1 });
}}
>
update
</button>
<br />
<br />
<Test />
<br />
<Result />
</div>
);
}
// Test.tsx
import { memo } from "react";
import { useStore } from "./utils";
export const Test: React.FC = memo(() => {
const [{ name }, updateStore] = useStore((store) => [store.name]);
// 打印 组件是否更新
console.log("rerender from Test");
return (
<div>
Test
<div>name: {name}</div>
<button
onClick={() => {
updateStore({ name: name + 1 });
}}
>
Update from Test
</button>
</div>
);
});
// Result.tsx
import { memo } from "react";
import { useStore } from "./utils";
export const Result: React.FC = memo(() => {
const [{ count, name }, updateStore] = useStore();
// 打印 组件是否更新
console.log("rerender from Result");
return (
<div>
From Result
<br />
count: {count}
<br />
name: {name}
</div>
);
});
看一下效果(因为掘金不能传视频,所以只有截图):
想要看完整的效果可以点击 Demo 的链接,进去体验一下。
最后补充上 ClassComponent
的实现和用法:
// 就是实现一个高阶组件 然后通知订阅的组件更新触发器
// mapStateToProps: (store) => ({count: store.count})
export function withStore(mapStateToProps) {
return function HOC<T>(WrapperComponent: React.ComponentClass<T>) {
class Wrapper extends Component<T, { exe: number }> {
keys: string[];
constructor(props: T) {
super(props);
this.keys = Object.keys(mapStateToProps(store));
console.log(this.keys);
this.state = {
exe: 0,
};
}
componentDidMount(): void {
manager.subscript((changed: KeysType) => {
(this.keys.some((key) => changed.includes(key)) ||
!this.keys.length) &&
this.setState({ exe: this.state.exe + 1 });
});
}
componentWillUnmount(): void {
// TODO manager.removeListener()...
}
// 自组件通过这个方法 updateStore & 触发更新
updateStore = (val: Partial<T>) => {
store = {
...store,
...val,
};
console.log(`listener's length: ${manager.listeners.length}`);
manager.notyfy(this.keys);
};
render(): ReactNode {
return (
<WrapperComponent
{...this.props}
{...mapStateToProps(store)}
updateStore={this.updateStore}
/>
);
}
}
return Wrapper;
};
}
// Test.tsx
import { Component, PureComponent, ReactNode } from "react";
import { withStore } from "./store";
class Test extends PureComponent {
render(): ReactNode {
console.log("rerender from Other");
const { name, updateStore } = this.props;
return (
<div>
Other
<br />
name: {name}
<br />
<button onClick={() => {
// update store
updateStore({name: name+1});
}}>
Update from Test
</button>
</div>
);
}
}
// 将store注入Test的props中 高阶组件中触发更新逻辑
export default withStore((store) => ({ name: store.name }))(Test);
至此我们已经实现了一个最简单实用的全局状态管理了。虽然已经有很多其他成熟的状态管理库,但是实现一个状态管理的库的话,基本思路都类似,这里重要的把思路理清楚。