简单的计数器组件
在下面的代码定义了一个简单的计数器组件
Counter
,从 0 开始计数,每点击一次计数器加一:
const Counter = ({ text }) => {
const [count, setCount] = useState(0);
const addCount = () => setCount(count + 1);
return (
<div onClick={addCount}>
{text}: {count}
</div>
我们可以对上面的 Counter
做进一步抽象,用一个自定义的 Hooks useCounter
来实现计数逻辑:
const useCounter = (initCount) => {
const [count, setCount] = useState(initCount || 0);
const addCount = () => setCount(count + 1);
return [count, addCount];
const Counter = ({ text }) => {
const [count, addCount] = useCounter(0);
return (
<div onClick={addCount}>
{text}: {count}
</div>
两段代码实现的计数器组件功能有任何区别,我们使用 Counter
组件来实现两个计数器:
const App = () => {
return (
<Counter text="计数器1" />
<Counter text="计数器2" />
</div>
代码示例如下:
计数器支持初始值
上面示例中的两个计数器之间是独立计数的,没有任何关联。接下来对 Counter
组件做一些改造,增加了一个 props 参数 initCount
,可以通过 initCount
来设置计数器的初始值。
const useCounter = (initCount) => {
const [count, setCount] = useState(initCount || 0);
const addCount = () => setCount(count + 1);
useEffect(() => {
setCount(initCount);
}, [initCount]);
return [count, addCount];
const Counter = ({ initCount, text }) => {
const [count, addCount] = useCounter(initCount);
return (
<div onClick={addCount}>
{text}: {count}
</div>
const App = () => {
return (
<Counter initCount={1} text="计数器1" />
<Counter initCount={0} text="计数器2" />
</div>
代码示例如下:
计数器之间同步更新
如果要求两个计数器同步计数,也就是其中一个计数器被点击时,两个计数器的数字保持一致且同时加一,该如何实现呢?很明显在这种场景下,可以让两个计数器来共享父组件中计数状态,需要对组件做进一步调整,除了能够接收父组件的计数状态之外,还要能够接收父组件修改计数的方法。在这里增加了 onChange
:
const useCounter = (initCount, onChange) => {
const [count, setCount] = useState(initCount || 0);
const addCount = onChange || (() => setCount(count + 1));
useEffect(() => {
setCount(initCount);
}, [initCount]);
return [count, addCount];
const Counter = ({ initCount, onChange, text }) => {
const [count, addCount] = useCounter(initCount, onChange);
return (
<div onClick={addCount}>
{text}: {count}
</div>
在 App
中可以实现两个计数器组件来同时更新计数:
const App = () => {
const [count, setCount] = useState(0);
const addCount = () => setCount(count + 1);
return (
<div className="App">
<Counter initCount={count} text="计数器1" onChange={addCount} />
<Counter initCount={count} text="计数器2" onChange={addCount} />
</div>
上面这段代码可以进一步优化为:
const App = () => {
const [count, addCount] = useCounter(0)
return (
<div className="App">
<Counter initCount={count} text="计数器1" onChange={addCount} />
<Counter initCount={count} text="计数器2" onChange={addCount} />
</div>
代码示例如下:
到这里,我们通过父子组件共享状态的方式,实现了组件间通信。接下来将通过一种简单的状态管理,来实现同样的功能。
计数器之间的状态管理
我们在 useCounter
基础上进一步做改造,那么要做哪些改造呢?
首先,将 initCount
作为一个全局变量,这样每个计数器都使用它的值。
然后,当组件更新自己的计数时,需要更新全局的 initCount
最后,当 initCount
发生变化时,各个计数器要拿到别人最新计数
const useCounter = (initCount, onChange) => {
const [count, setCount] = useState(initCount || 0);
const addCount = onChange || (() => setCount(count + 1));
useEffect(() => {
setCount(initCount);
}, [initCount]);
return [count, addCount];
这里将 useCounter
进行改写为 useGlobalCounter
:
let initCount = 0;
const useGlobalCounter = () => {
const [count, setCount] = useState(initCount || 0);
const addCount = () => {
initCount += 1;
useEffect(() => {
setCount(initCount);
}, []);
return [count, addCount];
很显然,我们需要在 addCount
中更新 initCount
, 并且要让其他的计数器感知到数据发生变化,其他计数器一旦感知到变化后要渲染最新的计数。这是一种典型的发布订阅场景,addCount
中发布数据更新的消息,在 useEffect
中订阅数据的变化。于是上面的代码可以进一步完善:
let initCount = 0;
const listeners = new Set();
const useGlobalCounter = () => {
const [count, setCount] = useState(initCount || 0);
const addCount = () => {
initCount += 1;
listeners.forEach((listener) => listener());
useEffect(() => {
const listener = () => {
setCount(initCount);
listeners.add(listener);
listener();
return () => {
listeners.delete(listener);
}, []);
return [count, addCount];
对 Counter
和 APP
做相应的调整:
const Counter = ({ initCount, text }) => {
const [count, addCount] = useGlobalCounter(initCount);
return (
<div onClick={addCount}>
{text}: {count}
</div>
const App = () => {
return (
<Counter text="计数器1" />
<Counter text="计数器2" />
</div>
代码示例如下:
这样,就实现了两个计数器同步更新的功能,与父子组件之间的状态同步效果类似。但是这个实现里面有个弊端,只限于计数器之间共享状态,那么如何做得更通用一些呢?
更通用的全局状态管理
接下来,对上面的代码进一步升级改造为 createGlobalState
,就可以实现一个简单的全局状态管理工具:
const createGlobalState = (initialState) => {
let globalState = initialState;
const listeners = new Set();
const setGlobalState = (nextGlobalState) => {
globalState = nextGlobalState;
listeners.forEach(listener => listener());
const useGlobalState = () => {
const [state, setState] = useState(globalState);
useEffect(() => {
const listener = () => {
setState(globalState);
listeners.add(listener);
listener();
return () => listeners.delete(listener);
}, []);
return [state, setGlobalState];
return {
setGlobalState,
useGlobalState,
使用 createGlobalState
来进行状态管理:
const { useGlobalState } = createGlobalState(0);
const Counter = ({ text }) => {
const [state, setGlobalState] = useGlobalState();
return (
<div onClick={() => setGlobalState(state + 1)}>
{text}: {state}
</div>
const App = () => {
const [state] = useGlobalState();
return (
<div className="App">
<Counter text="计数器" />
<div>点击次数:{state}</div>
</div>
代码示例如下:
这里 createGlobalState
实现相对比较简单,功能不够完善。如果感兴趣的话可以了解一下 react-hooks-global-state 这个状态管理工具,本文中介绍的实现思路来源于这个库。
微信搜索 ikoofe, 关注公众号「KooFE前端团队」关注前端技术动态。