13行代码实现状态管理工具

1,235 阅读5分钟

本文需要先行了解 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 还处于襁褓状态,很多地方还有需要完善的地方,还请大家多多支持。