3

使用 React Context API 和 Hooks 实现全局状态管理和性能优化

 2 years ago
source link: https://sanonz.github.io/2020/state-management-and-performance-optimization-for-react-context-api-with-hooks/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

使用 React Context API 和 Hooks 实现全局状态管理和性能优化

我们知道 React.js 默认没有全局状态的概念,需要安装第三方库来实现,最早的是流行的是 Facebook 自己出的 Flux,因为 Flux 使用流程有点复杂,后来 ReduxMobX 就兴起了。Redux 是借鉴 Flux 开发的,它们都是单向数据流,而 MobX 则有所不同,它是基于观察者模式实现。

虽然默认没有全局状态管理,但是也可以通过 Context 特性拼凑出来一个,那为啥以前没人拼凑一个出来用呢?那是因为 React.js 以前的 Context 不好用,也不稳定,官方不建议使用,所以一般是特殊情况非得用不可的时候才使用它,但是现在时过境迁,当初那个不成熟的 Context 现在已经变得强壮有力了。

在去年二月 React.js 发布了一个大的版本更新 v16.8.0 加入了 hooks 功能,其中内置了 useReducer() hook,它是 useState() 的替代品,简单的状态可以直接使用 useState,当我们遇到复杂多层级的状态或者下个状态要依赖上个状态时使用 useReducer() 则非常方便,在配合 ContextuseContext() 就能实现类似 Redux 库的功能。

实现全局状态

useReducer 的简单使用

这里借用了官方写的一个简单的示例,创建 Counter.jsx 文件。

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 文件。

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 文件,把更新状态的行为放到此组件中。

import React from 'react';
import { useStore } from './store';

function Header() {
const [/* state */, dispatch] = useStore();
console.log('header udpate');

return (
<>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}

export default Header;

创建 Footer.jsx 文件,把引用全局计数状态的放到此组件中。

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 /> 组件。

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 /> 组件中。这里放了一个演示窗口。

全局状态优化

性能问题排查

在上方演示中点击 + 按钮并注意控制台的打印,会有以下输出,其中前两个是组件初始化所打印的,后两个是我们点击 + 号按钮打印的,为了方便查看我在它们中间加了个换行,思考以下有什么性能问题呢?

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 文件。

    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() 钩子。

    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() 钩子。

    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 实现全局状态管理的话可以试下这个优化点,在实际开发中能为我们省下无数根头发。

至此结束,感谢阅读。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK