介绍

我们知道 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 文件。

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 [/* 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 文件,把引用全局计数状态的放到此组件中。

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

至此结束,感谢阅读。