简单的计数器组件

在下面的代码定义了一个简单的计数器组件 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 }) => {
  // 使用自定义 Hooks useCounter
  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);
  // 组件更新自己的计数,每次执行时 count 加 1
  const addCount = () => setCount(count + 1);
  // 拿外面的最新计数,props 中的 initCount 发生变化,则重置计数
  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 发生变化时,各个计数器要拿到别人最新计数
  • // useCounter 代码
    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
        initCount += 1;
        // 告知大家 initCount 发生变化,让大家更新计数
      useEffect(() => {
        // 拿 initCount 的最新计数更新状态
        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
        initCount += 1;
        // 告知大家 initCount 发生变化,让大家更新计数
        listeners.forEach((listener) => listener());
      useEffect(() => {
        const listener = () => {
          // 拿 initCount 的最新计数更新状态
          setCount(initCount);
        // 在 initCount 更新时调用 listeners 中每个 listener
        listeners.add(listener);
        // 避免在 add 之前 initCount 已经发生变化
        listener();
        return () => {
          listeners.delete(listener);
      }, []);
      return [count, addCount];
    

    CounterAPP 做相应的调整:

    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前端团队」关注前端技术动态。

    分类:
    前端
    标签: