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

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

转载于:https://juejin.im/post/5cfdc152f265da1b6028f737

猜你喜欢

转载自blog.csdn.net/weixin_34167043/article/details/93163477