惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
S
Secure Thoughts
月光博客
月光博客
美团技术团队
雷峰网
雷峰网
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Forbes - Security
Forbes - Security
W
WeLiveSecurity
P
Proofpoint News Feed
阮一峰的网络日志
阮一峰的网络日志
爱范儿
爱范儿
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
AI
AI
Last Week in AI
Last Week in AI
Google Online Security Blog
Google Online Security Blog
Schneier on Security
Schneier on Security
云风的 BLOG
云风的 BLOG
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recent Announcements
Recent Announcements
Webroot Blog
Webroot Blog
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
N
News and Events Feed by Topic
罗磊的独立博客
The Register - Security
The Register - Security
Blog — PlanetScale
Blog — PlanetScale
T
Threat Research - Cisco Blogs
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
人人都是产品经理
人人都是产品经理
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
B
Blog
腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Engineering at Meta
Engineering at Meta
Latest news
Latest news
IT之家
IT之家
D
DataBreaches.Net
博客园 - 司徒正美
N
Netflix TechBlog - Medium
V
V2EX
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Sanonz

使用 Invox Setup 实现 Electron 安装包界面美化 用 VS Code 编辑器开发 STM32 嵌入式 使用 FontCreator 编辑字体库 | Sanonz 使用 Phonegap + Cordova 搭建混合开发平台 利用 Github Actions 自动部署 Hexo 博客 在 WEB 端实现亮/暗主题跟随系统功能 | Sanonz Metalness vs Specular Workflows for PBR Shading 以太坊概念与使用 wep3/infura 实现以太币及合约币交易流程 | Sanonz 实现树莓派(Raspberry Pi)联网发送本机IP到绑定的微信 | Sanonz 使用 Three.js 的 RenderTarget 实现离屏渲染 使用 redux-store-provider 简化 react 开发流程 浏览器 HTTP 并发请求规则探讨 | Sanonz JavaScript Event Loop、Micro Task、Macro Task WebAssembly 从入门到吃鸡 | Sanonz Let's Encrypt 使用教程,免费的 SSL 证书,让你的网站拥抱 HTTPS Let's Encrypt 使用教程,免费的 SSL 证书,让你的网站拥抱 HTTPS Ubuntu 搭建 php、mysql、nginx 开发环境 | Sanonz WebGL 三维教程 - 绘制矩形和点(二) | Sanonz WebGL 三维教程 - WebGL 介绍(一) Hexo 搭建静态博客与 hexo-theme-concise 主题使用教程 | Sanonz
使用 React Context API 和 Hooks 实现全局状态管理和性能优化
Sanonz · 2020-09-09 · via Sanonz

介绍

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

至此结束,感谢阅读。