介绍
我们知道 React.js 默认没有全局状态的概念,需要安装第三方库来实现,最早的是流行的是 Facebook 自己出的 Flux,因为 Flux 使用流程有点复杂,后来 Redux、MobX 就兴起了。Redux 是借鉴 Flux 开发的,它们都是单向数据流,而 MobX 则有所不同,它是基于观察者模式实现。
虽然默认没有全局状态管理,但是也可以通过 Context 特性拼凑出来一个,那为啥以前没人拼凑一个出来用呢?那是因为 React.js 以前的 Context 不好用,也不稳定,官方不建议使用,所以一般是特殊情况非得用不可的时候才使用它,但是现在时过境迁,当初那个不成熟的 Context 现在已经变得强壮有力了。
在去年二月 React.js 发布了一个大的版本更新 v16.8.0 加入了 hooks 功能,其中内置了 useReducer() hook,它是 useState() 的替代品,简单的状态可以直接使用 useState,当我们遇到复杂多层级的状态或者下个状态要依赖上个状态时使用 useReducer() 则非常方便,在配合 Context 与 useContext() 就能实现类似 Redux 库的功能。
实现全局状态
useReducer 的简单使用
这里借用了官方写的一个简单的示例,创建 Counter.jsx 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | const initialState = {count: 0};
  function reducer(state, action) {   switch (action.type) {     case 'increment':       return {count: state.count + 1};     case 'decrement':       return {count: state.count - 1};     default:       throw new Error();   } }
  function Counter() {   const [state, dispatch] = useReducer(reducer, initialState);   return (     <>       Count: {state.count}       <button onClick={() => dispatch({type: 'decrement'})}>-</button>       <button onClick={() => dispatch({type: 'increment'})}>+</button>     </>   ); }
  | 
这样就实现了一个简单的 redux 方式的状态管理器,目前这种只是替代 useState() 在组件中绑定使用的方式,下边将会介绍提升到全局作为全局状态来使用。
借助 Context 实现全局状态
创建 store.jsx 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
   | import React, { createContext, useReducer, useContext } from 'react';
  const initialState = {count: 0};
  function reducer(state, action) {   switch (action.type) {     case 'increment':       return {count: state.count + 1};     case 'decrement':       return {count: state.count - 1};     default:       throw new Error();   } }
  const Context = createContext();
  function useStore() {   return useContext(Context); }
  function StoreProvider({ children }) {   const [state, dispatch] = useReducer(reducer, initialState);
    return (     <Context.Provider value={[state, dispatch]}>       {children}     </Context.Provider>   ); }
  export { useStore, StoreProvider };
  | 
创建 Header.jsx 文件,把更新状态的行为放到此组件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | import React from 'react'; import { useStore } from './store';
  function Header() {   const [, dispatch] = useStore();   console.log('header udpate');
    return (     <>       <button onClick={() => dispatch({type: 'decrement'})}>-</button>       <button onClick={() => dispatch({type: 'increment'})}>+</button>     </>   ); }
  export default Header;
   | 
创建 Footer.jsx 文件,把引用全局计数状态的放到此组件中。
1 2 3 4 5 6 7 8 9 10 11 12 13
   | import React from 'react'; import { useStore } from './store';
  function Footer() {   const [state] = useStore();   console.log('footer udpate');
    return (     <p>{state.count}</p>   ); }
  export default Footer;
   | 
创建 App.jsx 文件,用 <StoreProvider /> 组件包装 <Header /> 与 <Footer /> 组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | import React from 'react';
  import Header from './Header'; import Footer from './Footer'; import { StoreProvider } from './store';
  function App() {   return (     <StoreProvider>       <div>         <Header />         <Footer />       </div>     </StoreProvider>   ); }
  export default App;
   | 
这样我们就实现了全局 Store,在需要使用全局状态的地方调用 useStore() 就可以使用状态以及更改状态。为了方便查看引用 useStore() hook 的组件的更新状况,我们把更新行为放到了 <Header /> 组件中,把引用计数的放到了 <Footer /> 组件中。这里放了一个演示窗口。
全局状态优化
性能问题排查
在上方演示中点击 + 按钮并注意控制台的打印,会有以下输出,其中前两个是组件初始化所打印的,后两个是我们点击 + 号按钮打印的,为了方便查看我在它们中间加了个换行,思考以下有什么性能问题呢?
1 2 3 4 5
   | header udpate footer udpate
  header udpate footer udpate
   | 
在 <Header /> 组件中我们并没有使用 state 状态,只是使用了更新方法 dispatch 而已,但是当状态更新时 <Header /> 组件依然执行了重绘,当我们每次点击 +、- 按钮时 <Header /> 组件都会重绘,但是实际上这个重绘显然是不需要的。
在实际开发中,我们可能会在很多组件中使用 const [, dispatch] = useStore() 这种方式,只是使用了 useStore() 的 dispatch 方法,React 的机制是只要有组件调用了 useStore() 钩子,state 变化时此组件都会重绘,和是否使用 state 没有关系,这样我们的很多只引用了 dispatch 方法的组件都会执行重绘,引用的组件越多重绘计算就变得越是非常的浪费,那怎么解决呢?
减少不必要的组件重绘
useStore() 方法是我们为了方便调用封装的一个钩子,它的背后执行的是 useContext(Context),也就是每当 <Context.Provider value={[state, dispatch]} /> 的 value 变化时,就会重绘对应引用 useContext(Context) 钩子的组件,知道了原因接下来就是解决问题了。
既然 [state, dispatch] 并不一定会一块使用,但会一块更新,那我们就把 <Context.Provider value={[state, dispatch]} /> 拆分成两个 Context 就能解决此问题,一个 <StateContext.Provider value={state} />,另一个为 <DispatchContext.Provider value={dispatch} />,然后分别封装 useStateStore() 与 useDispatchStore() 钩子,这样的话 state 变动时只调用 useDispatchStore() 钩子的组件并不会做多余的重绘,具体优化如下。
编辑 store.jsx 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
   |     import React, { createContext, useReducer, useContext } from 'react';
      const initialState = {count: 0};
      function reducer(state, action) {       switch (action.type) {         case 'increment':           return {count: state.count + 1};         case 'decrement':           return {count: state.count - 1};         default:           throw new Error();       }     }
  -   const Context = createContext();
  -   function useStore() { -     return useContext(Context); -   }
  +   const StateContext = createContext(); +   const DispatchContext = createContext();
  +   function useStateStore() { +     return useContext(StateContext); +   }
  +   function useDispatchStore() { +     return useContext(DispatchContext); +   }
      function StoreProvider({ children }) {       const [state, dispatch] = useReducer(reducer, initialState);
        return ( -       <Context.Provider value={[state, dispatch]}> -         {children} -       </Context.Provider> +       <StateContext.Provider value={state}> +         <DispatchContext.Provider value={dispatch}> +           {children} +         </DispatchContext.Provider> +       </StateContext.Provider>       );     }
  -   export { useStore, StoreProvider }; +   export { useStateStore, useDispatchStore, StoreProvider };
  | 
修改 Header.jsx 文件,只调用 useDispatchStore() 钩子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   |     import React from 'react'; -   import { useStore } from './store'; +   import { useDispatchStore } from './store';
      function Header() { -     const [/* state */, dispatch] = useStore(); +     const dispatch = useDispatchStore();       console.log('header udpate');
        return (         <>           <button onClick={() => dispatch({type: 'decrement'})}>-</button>           <button onClick={() => dispatch({type: 'increment'})}>+</button>         </>       );     }
      export default Header;
   | 
修改 Footer.jsx 文件,只调用 useStateStore() 钩子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   |     import React from 'react'; -   import { useStore } from './store'; +   import { useStateStore } from './store';
      function Footer() { -     const [state] = useStore(); +     const state = useStateStore();       console.log('footer udpate');
        return (         <p>{state.count}</p>       );     }
      export default Footer;
   | 
当我们再次运行时点击 +、- 按钮只会重绘引用 useStateStore() 的组件,而引用 useDispatchStore() 的组件则不会跟随重绘,效果如下。
小结
如果你们的项目直接使用 Context 和 Hooks 实现全局状态管理的话可以试下这个优化点,在实际开发中能为我们省下无数根头发。
至此结束,感谢阅读。