介绍
我们知道 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 实现全局状态管理的话可以试下这个优化点,在实际开发中能为我们省下无数根头发。
至此结束,感谢阅读。