本文需要先行了解 Hooks 的基础知识。
React 状态管理实现有两种,一种是 Flux 架构的,例如 Redux,其通过 Context 实现全局状态共享;另一种是响应式的,例如 Mobx,通过可观测对象和 HOC 实现状态共享。
在 Hooks 出来后,之前的通过 props 的状态解决方案就有些过于繁琐了,鉴于之前的状态管理的复杂,前两天我写了一个状态管理工具 Piex Store,完全面向对象,基于 Hooks,不借助于 Context 实现状态共享。
本文将基于其核心原理,逐步实现一个最简单的状态管理工具,其核心代码只有 13 行。
Custom Hook
Hooks API 出来后,Function Component(函数组件,以下简称 FC) 也有了自己的状态,而且可以自定义 Custom Hook,这便让我们对组件状态有个更多的操作可能性。以下是一个简单的自定义 Hooks:
const useCounter = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
const decrement = useCallback(() => {
setCount(count - 1);
}, [count]);
return {count, increment, decrement};
}
如果要使用的话需要在一个 FC 里:
const Counter = () => {
const {count, increment, decrement} = useCounter();
return (
<article>
<p>{count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</article>
)
}
这样我们就实现了一个简单的 custom hooks,可以复用对 count 的操作逻辑。
但是这里有一个问题,大家可以运行这个 例子,我们使用两个 Counter 组件的话,它们的 count 是互不关联的,两者之间没有任何关系。如果我们有办法让 count 共享,每次修改状态就能在所有用到的地方同步到状态,不就是状态共享吗!
控制组件更新
共享状态的一个问题就是如何把状态同步到所有组件并更新页面,其实通过 useState 就可以轻松实现,下面我们看一个例子:
const App = () => {
const [,setState] = useState(Math.random());
setTimeout(()=>{
setState(Math.random());
}, 200);
return (
<p>{Date.now()}</p>
)
};
点击 这里 查看实际运行效果。
可以发现这里没有取 useState 的第一个参数,而只是用了第二个参数更新状态,实际上每 200ms 后页面上就显示不同的时间戳。
其实 useState 并不是什么黑魔法,具体实现原理可以看我的 这篇文章。简单来讲,就是我们每次 setState 时,如果参数和前一个 state 不相等,React 就会重新运行函数组件,把返回的值做 DOM Diff 来更新页面,所以关键就在于 setState,如果我们掌握了 setState,就掌握了更新组件的时机。
状态共享
我们通过上面的例子知道 useState 返回数组的第二个值,这里称为 setState,可以控制组件的渲染。
那么如果有一个方法,把所有用到共享状态的组件都创建一个 useState,并把第二个参数存储起来,每次更新共享状态时,把所有的 setState 都运行一遍,那么不就可以更新所有组件的状态了吗?不就实现状态共享了吗?
下面我们通过一个简单的例子看一下:
let _count = 0;
let _setters = [];
const useCounter = () => {
const [, setCount] = useState(_count);
_setters.push(setCount);
const increment = useCallback(() => {
_count++;
_setters.forEach(setState => setState(_count));
}, [_count]);
const decrement = useCallback(() => {
_count--;
_setters.forEach(setState => setState(_count));
}, [_count]);
return {count:_count, increment, decrement};
}
const Counter = () => {
const {count, increment, decrement} = useCounter();
return (
<article>
<p>{count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</article>
)
}
const App = () => {
return (
<div>
<Counter />
<Counter />
</div>
)
}
运行效果点击 这里 查看。
这个例子是基于第一个 custom hook 的例子改动的,可以发现点击一个按钮,页面上两个 Counter 组件的 count 值都变了。
这是因为我把 useCounter 的状态存到全局的 _count 变量中了,并且把 useState 的第二个参数也都收集到全局的 _setters 数组中了,每次操作 increment 或者 decrement 时,就会先改变 _count 的值,然后触发 setters 中的 setState,这样所有 Counter 组件都会更新啦。而且返回的是 _count 变量重命名为 count,所以每个组件的 setState 被触发后都会通过 _count 得到最新的值并显示在页面上。
这样我们就实现了一个最简单的计数器状态共享,但是每次都自己写太麻烦了,可以设计一个简单易用,立马可以上手的通用工具使用。
通用工具
设计一个通用工具需要看应用场景和通用模型,面向对象是一个不错的选择。关于更多细节可以看 Piex Store 核心概念 来了解,我们看一下怎么实现:
export abstract class Store {
state = {};
setters = [];
setState(newState) {
this.state = newState;
this.setters.forEach(setState => setState(this.state));
}
}
export function useStore(store) {
let [, setState] = useState(store.state);
store.setters.push(setState);
return store;
}
由于 JS 不支持继承,所以这段代码用 TS 实现,去掉空行,短短 13 行代码就实现了一个状态管理:
state对应上例中的_count,存储全局状态;setters对应上例中的_setters,收集setState;setState方法用来更新组件;useStore则用来收集依赖;
具体怎么使用呢?如下:
class CounterStore extends Store {
state = {
count: 0,
}
increment() {
this.setState({
count: this.state.count + 1,
})
}
decrement() {
this.setState({
count: this.state.count + 1,
})
}
}
const counterStore = new CounterStore();
const Counter = () => {
const store = useStore(counterStore);
return (
<article>
<p>{store.state.count}</p>
<button onClick={store.increment}>Increment</button>
<button onClick={store.decrement}>Decrement</button>
</article>
)
}
const App = () => {
return (
<div>
<Counter />
<Counter />
</div>
)
}
这样,所有用到 counterStore 的地方都可以共享 counterStore.state 的变量,还可以通过对象方法来更新 state 并同步到其它组件。
最后
当然,这只是一个简单的 demo,很多东西都没有做,如 setters 收集的 setState 依赖在组件卸载时释放;全量更新状态太麻烦,仅部分更新就可以了,还有数据变更检测等等。
这些肯定不是 13 行代码可以实现的了,如果感兴趣可以看 Piex Store,基于上述原理实现的状态管理工具,完美支持 TS 类型推断,遵从 React 设计哲学,支持中间件,可以使用 Redux DevTools 观察状态变更。
由于 Piex Store 还处于襁褓状态,很多地方还有需要完善的地方,还请大家多多支持。