React性能优化
注意:此文档还在修改状态。
组件缓存
由于 React 并不知道在父组件中的更新是否会影响到其子代,所以 React 会在父组件更新时,将其所有子代也重绘,这会导致很大的性能消耗。为了减少这种不必要的性能损耗,我们可以使用缓存的方式处理子组件。这样当props
浅层比较的结果相同时,父组件发生变化时,React 会去缓存中重用子组件先前的渲染结果,而不是重新渲染子组件。
使用React.memo()做组件缓存。
下面我们来对比看下使用缓存前后对性能的影响:
使用前:
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
interface Props {
title: string;
}
function Child(props: Props) {
return <div>{props.title}</div>;
}
function Parent() {
const [, setCount] = useState(0);
useEffect(() => {
// 每隔一秒钟重绘一次组件
setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
}, []);
return (
<div>
<Child title="child1" />
<Child title="child2" />
</div>
);
}
ReactDOM.render(<Parent />, document.getElementById('root'));
Parent
组件每隔一秒钟发生一次状态变更,两个Child
组件也会发生重绘,如下面的 React 渲染火焰图所示(绿色方格代表发生了重绘):
为了避免不必要的渲染,我们可以对上述示例稍加调整:
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
interface Props {
title: string;
}
function Child(props: Props) {
return <div>{props.title}</div>;
}
const MemoChild = React.memo(Child);
function Parent() {
const [, setCount] = useState(0);
useEffect(() => {
// 每隔一秒钟重绘一次组件
setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
}, []);
return (
<div>
<MemoChild title="child1" />
<MemoChild title="child2" />
</div>
);
}
ReactDOM.render(<Parent />, document.getElementById('root'));
这里我们使用React.memo做了组件缓存,因此在Parent
组件状态发生变化时,两个Child
组件因为属性没有发生变化,React 会从缓存中取其上次的渲染结果,而不是重新传染。渲染效果如下(火焰图中的两个Memo(Child)
小方格都是灰色的,代表未发生重绘):
props
浅层比较
上文提到,React.memo(ChildComponent)
的缓存依赖于props
浅层比较,在两次重绘中,如果给ChildComponent
的属性对象通过浅层比较是一致的,那么 React 会从缓存中取上次的渲染结果,缓存有效。我们看看什么是浅层比较。
示例:
const objA = {
a: '1',
b: '2',
};
const objB = {
a: '1',
b: '2',
};
Object.keys(objA).every((key) => objA[key] === objB[key]); // true
对象objA
和objB
都具有同样值的属性,所以这两个对象是相同的。但如果对象中再有对象属性,那么很有可能就不再相等,因为浅层比较只会比较对象的属性值是否相等,而如果对象属性也是对象,浅层比较不会进入这个属性对象再比较。如下所示:
const objA = {
a: '1',
b: '2',
c: {
t: '3',
},
};
const objB = {
a: '1',
b: '2',
c: {
t: '3',
},
};
Object.keys(objA).every((key) => objA[key] === objB[key]); // false
上面两个对象的浅层比较结果为false
,因为objA.c
和objB.c
都是对象,但是objA.c !== objB.c
。
接下来,我们来看看各种类型的属性如何配合React.memo(),达到缓存的目的。
函数属性
函数属性是常见的一种类型的属性,处理不好函数类型属性,极容易破坏掉属性浅层比较,让组件缓存失效。
比如:
const MemoButton = React.memo(Button);
function ButtonDemo() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1);
};
return <div>
<MemoButton onClick={handleClick}>点击我</MenuButton>
<span>被点击次数:{count}</span>
</div>;
}
每次点击按钮时,本期望MemoButton
组件不会发生重绘,实际上却每次都发生了重绘。重点在于每次重绘ButtonMenu
时,都会产生新的handleClick
。
我们根据函数属性本身与组件是否有关系分成两类:
- 动态函数属性 - 函数实现与组件相关,需要在组件内部定义。如按钮点击事件处理器。
- 静态函数属性 - 函数实现与组件无关,可以在组件外部定义。如表单值校验函数。
我们逐个看看这两种情况如何处理。
动态函数属性
这里我们首先来看一个示例:
import React, { useState, ChangeEvent } from 'react';
import ReactDOM from 'react-dom';
function Input(props: {
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}) {
return <input type="text" value={props.value} onChange={props.onChange} />;
}
const TextInput = React.memo(Input);
function Demo() {
const [value1, setValue1] = useState('');
const [value2, setValue2] = useState('');
const onChangeValue1 = (event: ChangeEvent<HTMLInputElement>) => {
setValue1(event.target.value);
};
const onChangeValue2 = (event: ChangeEvent<HTMLInputElement>) => {
setValue2(event.target.value);
};
return (
<div>
<TextInput value={value1} onChange={onChangeValue1} />
<TextInput value={value2} onChange={onChangeValue2} />
</div>
);
}
ReactDOM.render(<Demo />, document.getElementById('root'));
思考一个问题:此时如果改变第二个TextInput
值,第一个TextInput
会重新渲染吗?
一定会的,看下渲染结果:
可能你会有疑问:不是已经用了React.memo
缓存了吗?为什么还会重新渲染呢?
其实这是因为TextInput
的onChange
属性,这是一个用来监听值变化的回调函数,输入框的值发生改变时,Demo
组件会重绘,而此时onChange
属性会指向一个新的方法,此时TextInput
的属性浅层比较会返回false
,组件就会重新渲染。
解决上述问题有两种方式:
方式一:使用React.useCallback缓存回调函数
import React, { useState, ChangeEvent, useCallback } from 'react';
import ReactDOM from 'react-dom';
function Input(props: {
value?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}) {
return <input type="text" value={props.value} onChange={props.onChange} />;
}
const TextInput = React.memo(Input);
function Demo() {
const [value1, setValue1] = useState('');
const [value2, setValue2] = useState('');
const onChangeValue1 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue1(event.target.value);
}, []);
const onChangeValue2 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue2(event.target.value);
}, []);
return (
<div>
<TextInput value={value1} onChange={onChangeValue1} />
<TextInput value={value2} onChange={onChangeValue2} />
</div>
);
}
ReactDOM.render(<Demo />, document.getElementById('root'));
上述示例中,useCallback
的依赖项数组为[]
,所以,onChangeValue1
与onChangeValue2
这两个方法只会在组件初始化时创建一次,后续组件值发生改变时,会直接使用它的memoized
版本。因此当其中一个TextInput
的值发生改变时,另一个TextInput
组件的属性满足浅层比较,React 会从缓存读取其上次渲染结果,而非重新渲染。
方式二:使用React.useReducer代替React.useState
管理状态
import React, { ChangeEvent, useReducer } from 'react';
import ReactDOM from 'react-dom';
import produce from 'immer';
interface Action {
type: string;
payload: any;
}
interface State {
[name: string]: any;
}
const reducer = produce((state: State, action: Action) => {
switch (action.type) {
case 'CHANGE_VALUE':
state[action.payload.name] = action.payload.value;
return state;
default:
return state;
}
});
function Input(props: any) {
const onChangeValue = (event: ChangeEvent<HTMLInputElement>) => {
props.dispatch({
type: 'CHANGE_VALUE',
payload: { name: props.name, value: event.target.value },
});
};
return <input type="text" value={props.value} onChange={onChangeValue} />;
}
const TextInput = React.memo(Input);
function Demo() {
const [state, dispatch] = useReducer(reducer, {});
return (
<div>
<TextInput value={state.value1} name="value1" dispatch={dispatch} />
<TextInput value={state.value2} name="value2" dispatch={dispatch} />
</div>
);
}
ReactDOM.render(<Demo />, document.getElementById('root'));
上述示例中,我们向子组件传递dispatch
而不是回调函数。这样当第一个TextInput
值发生改变时,第二个TextInput
组件的属性并没有发生改变,因此其不会重新渲染。
静态函数属性
当组件属性为一个静态函数时,为了减少不必要的渲染,我们可以将函数提升到组件外部,下面我们将通过两个实例对比提升前和提升后的影响。
组件内部处理:
import React, { useState, ChangeEvent, useCallback } from 'react';
import ReactDOM from 'react-dom';
function Input(props: any) {
const error = props.validate(props.value);
return (
<>
<input type="text" value={props.value} onChange={props.onChange} />
<p style={{ color: 'red' }}>{error}</p>
</>
);
}
const TextInput = React.memo(Input);
function Demo() {
const [value1, setValue1] = useState('');
const [value2, setValue2] = useState('');
const onChangeValue1 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue1(event.target.value);
}, []);
const onChangeValue2 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue2(event.target.value);
}, []);
const validate = (value: any) => {
if (!value) {
return '必填';
}
};
return (
<div>
<TextInput value={value1} onChange={onChangeValue1} validate={validate} />
<TextInput value={value2} onChange={onChangeValue2} validate={validate} />
</div>
);
}
ReactDOM.render(<Demo />, document.getElementById('root'));
此时只要任意一个输入框的值发生改变,父组件就会重新渲染,创建新的 validate 函数传给子组件,这样就破坏了属性浅层比较,子组件会重新渲染。
我们对上述示例稍加调整,将validate
提升至组件外部:
import React, { useState, ChangeEvent, useCallback } from 'react';
import ReactDOM from 'react-dom';
function Input(props: any) {
const error = props.validate(props.value);
return (
<>
<input type="text" value={props.value} onChange={props.onChange} />
<p style={{ color: 'red' }}>{error}</p>
</>
);
}
const TextInput = React.memo(Input);
function validate(value: any) {
if (!value) {
return '必填';
}
}
function Demo() {
const [value1, setValue1] = useState('');
const [value2, setValue2] = useState('');
const onChangeValue1 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue1(event.target.value);
}, []);
const onChangeValue2 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue2(event.target.value);
}, []);
return (
<div>
<TextInput value={value1} onChange={onChangeValue1} validate={validate} />
<TextInput value={value2} onChange={onChangeValue2} validate={validate} />
</div>
);
}
ReactDOM.render(<Demo />, document.getElementById('root'));
我们来看下渲染结果的分析图:
从火焰图中我们可以看出,当我们改变第一个TextInput
值时,第二个TextInput
并没有重新渲染。这是因为当我们把校验函数提升到组件外部,即全局作用域
中,所以无论Demo
内部如何变化,validate
都不会发生改变,这样第二个TextInput
组件的属性满足浅层比较,因此不会重新渲染。
数组、对象属性
不正确指定数组、对象属性,也会破坏掉React.memo
,看几个缓存失效的示例。
示例 1:
function ButtonDemo() {
return (
<MemoButton
style={{
color: 'red',
}}
>
点击我
</MemoButton>
);
}
示例 2:
function TodoDemo() {
const completedItems = items.filter((item) => item.completed);
return <List items={completedItems} />;
}
我们根据对象、数组的来源分成两个类别:
- 动态计算的对象属性 - 如示例 2 中的
completedItems
- 静态对象属性 - 如示例 1 中的
style
接下来我们分别看看怎么处理,以达到缓存的目的。
动态计算出的对象属性
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
function UserInfo(props: any) {
const { userName, age, duty, fav, birthday } = props.userInfo;
return (
<div>
<div>姓名:{userName}</div>
<div>年龄:{age}</div>
<div>爱好:{fav}</div>
<div>职务:{duty}</div>
<div>出生年份:{birthday}</div>
</div>
);
}
const User = React.memo(UserInfo);
const data = {
userName: '张三',
age: 19,
fav: '篮球、排球',
duty: '处员',
};
function Demo() {
const currentYear = new Date().getFullYear();
const [, setCount] = useState(0);
useEffect(() => {
// 每隔一秒钟重绘一次组件
setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
}, []);
const userInfo = {
...data,
age: `${data.age}岁`,
birthday: currentYear - data.age,
};
return <User userInfo={userInfo} />;
}
ReactDOM.render(<Demo />, document.getElementById('root'));
我们来思考一个问题:每次Demo
组件重绘时,UserInfo
会不会重绘?
很显然,答案是会的,因为Demo
每次重绘,都会定义一个新的userInfo
对象传给UserInfo
组件,不满足其属性浅层比较,因此UserInfo
会发生重绘。
这种情况很容易处理,我们只需要使用React.useMemo缓存一下userInfo
这个对象属性即可:
import React, { useState, useEffect, useMemo } from 'react';
import ReactDOM from 'react-dom';
function UserInfo(props: any) {
const { userName, age, duty, fav, birthday } = props.userInfo;
return (
<div>
<div>姓名:{userName}</div>
<div>年龄:{age}</div>
<div>爱好:{fav}</div>
<div>职务:{duty}</div>
<div>出生年份:{birthday}</div>
</div>
);
}
const User = React.memo(UserInfo);
const data = {
userName: '张三',
age: 19,
fav: '篮球、排球',
duty: '处员',
};
function Demo() {
const currentYear = new Date().getFullYear();
const [, setCount] = useState(0);
useEffect(() => {
// 每隔一秒钟重绘一次组件
setInterval(() => {
setCount((prev) => prev + 1);
}, 1000);
}, []);
const userInfo = useMemo(
() => ({
...data,
age: `${data.age}岁`,
birthday: currentYear - data.age,
}),
[data, currentYear],
);
return <User userInfo={userInfo} />;
}
ReactDOM.render(<Demo />, document.getElementById('root'));
此时,在父组件发生重绘时,子组件是不会重新渲染的,原因是因为每次重绘,currentYear
和data
都没发生改变,userInfo
一直都是第一次渲染时定义的那个,不会发生改变,这样就满足了UserInfo
组件的浅层比较,因此不会重新渲染。
静态对象属性
当组件属性为静态对象时,我们可以将其提升到组件外部的全局作用域,以此来满足子组件属性的浅层比较。这里我们给输入框添加 label,并指定其样式,通过对比下面两个示例,看下静态对象属性对组件渲染的影响:
直接传递静态对象属性
import React, { useState, ChangeEvent, useCallback } from 'react';
import ReactDOM from 'react-dom';
function Input(props: any) {
return (
<>
<label style={props.labelStyle}>{props.label}</label>
<input type="text" value={props.value} onChange={props.onChange} />
</>
);
}
const TextInput = React.memo(Input);
function Demo() {
const [value1, setValue1] = useState('');
const [value2, setValue2] = useState('');
const onChangeValue1 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue1(event.target.value);
}, []);
const onChangeValue2 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue2(event.target.value);
}, []);
return (
<div>
<TextInput
labelStyle={{ color: 'blue' }}
label="用户名"
value={value1}
onChange={onChangeValue1}
/>
<TextInput
labelStyle={{ color: 'blue' }}
label="密码"
value={value2}
onChange={onChangeValue2}
/>
</div>
);
}
ReactDOM.render(<Demo />, document.getElementById('root'));
改变一个输入框的值,由于labelStyle
为对象属性,不满足TextInput
属性的浅层比较,因此组件会重新渲染:
我们尝试将上述示例中的lableStyle
提升至Demo
组件外部的全局作用域中,再看下效果:
import React, { useState, ChangeEvent, useCallback } from 'react';
import ReactDOM from 'react-dom';
function Input(props: any) {
return (
<>
<label style={props.labelStyle}>{props.label}</label>
<input type="text" value={props.value} onChange={props.onChange} />
</>
);
}
const TextInput = React.memo(Input);
const labelStyle = { color: 'blue' };
function Demo() {
const [value1, setValue1] = useState('');
const [value2, setValue2] = useState('');
const onChangeValue1 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue1(event.target.value);
}, []);
const onChangeValue2 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue2(event.target.value);
}, []);
return (
<div>
<TextInput
labelStyle={labelStyle}
label="用户名"
value={value1}
onChange={onChangeValue1}
/>
<TextInput
labelStyle={labelStyle}
label="密码"
value={value2}
onChange={onChangeValue2}
/>
</div>
);
}
ReactDOM.render(<Demo />, document.getElementById('root'));
将labelStyle
提升至全局作用域,这样每次组件内部发生变化时,labelStyle
并没有变,满足TextInput
属性的浅层比较,因此在第一个TextInput
值发生改变时,父组件会从缓存中取第二个TextInput
的渲染结果,而不是重新渲染。
火焰图:
children 属性
JSX
的优化
静态静态JSX
优化,可以将静态的Jsx
部分提升到组件外部定义:
import React from 'react';
import ReactDOM from 'react-dom';
function Child(props: any) {
return <p>{props.children}</p>;
}
const childs = (
<>
<Child>123</Child>
<Child>这是文本</Child>
</>
);
function Parent() {
return <div>{childs}</div>;
}
ReactDOM.render(<Parent />, document.getElementById('root'));
在 React 应用中我们无需手动处理,可以借助react-constant-elements 工具完成,需要注意的是此插件只作用于生产环境。
使用工具度量 React 组件性能
react-devtools
这里我们主要是用 react-devtools 的火焰图来做性能分析。
我们以一个具体示例来说明:
import React, { useState, ChangeEvent, useCallback } from 'react';
import ReactDOM from 'react-dom';
function Input(props: any) {
const error = props.validate(props.value);
return (
<>
<input type="text" value={props.value} onChange={props.onChange} />
<p style={{ color: 'red' }}>{error}</p>
</>
);
}
const TextInput = React.memo(Input);
function validate(value: any) {
if (!value) {
return '必填';
}
}
function Demo() {
const [value1, setValue1] = useState('');
const [value2, setValue2] = useState('');
const onChangeValue1 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue1(event.target.value);
}, []);
const onChangeValue2 = useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue2(event.target.value);
}, []);
return (
<div>
<TextInput value={value1} onChange={onChangeValue1} validate={validate} />
<TextInput value={value2} onChange={onChangeValue2} validate={validate} />
</div>
);
}
ReactDOM.render(<Demo />, document.getElementById('root'));
react-devtools 使用方式:
- 运行上述示例
- 打开 F12 控制台,将页签切换至
React
- 将工具条从
Elements
切换至Profilter
- 点击小圆点开始分析,改变值使组件重绘点击
stop
按钮,收集分析结果
分析结果如下:
图中有三块区域:
- 组件渲染次数
- 每次渲染时相关组件的渲染情况(灰色代表不渲染,绿色或黄色代表重新渲染)
- 渲染信息,包括组件的总渲染次数,渲染耗时,组件属性等信息
从上图结果不难看出,当我们改变第一个输入框的值时,父组件发生重绘,此时第一个输入框也会重新渲染,第二个输入框组件则不会重新渲染。
performance 分析
这是 Chrome 浏览器自带的性能分析工具。
操作:
打开控制台,切换到Performance
页签,确保 screenshots checkbox 是选中的,然后点击 controls,开始记录(windows 快捷键 shift + E),这时候 Devtools 就开始录制各种性能指标,你可以点击页面等进行各种操作,所有的操作都会被浏览器录制下来。录制期间, 可以点击 stop 进行快速操作结束录制,然后等待显示性能报告,stop 按钮位置如下图:
分析结果如下:
上图一共有三个区域:
- overview 总览图,高度概括随时间线的变动,包括 FPS,CPU,NET
- 火焰图,从不同的角度分析框选区域 。例如:Network,Frames, Interactions, Main 等
- 总结区域:精确到毫秒级的分析,以及按调用层级,事件分类的整理
overview
Overview 窗格包含以下三个图表:
- FPS。每秒帧数。绿色竖线越高,FPS 越高。 FPS 图表上的红色块表示长时间帧,很可能会出现卡顿
- CPU。 CPU 资源。此面积图指示消耗 CPU 资源的事件类型
- NET。每条彩色横杠表示一种资源。横杠越长,检索资源所需的时间越长。 每个横杠的浅色部分表示等待时间(从请求资源到第一个字节下载完成的时间)
可以放大显示一部分记录,以便简化分析。使用 Overview 窗格可以放大显示一部分记录。 放大后,火焰图会自动缩放以匹配同一部分
火焰图
在火焰图上看到一到三条垂直的虚线。蓝线代表 DOMContentLoaded 事件。 绿线代表首次绘制的时间。 红线代表 load 事件
在火焰图中选择事件时,Details 窗格会显示与事件相关的其他信息。
总结区域
蓝色(Loading):网络通信和 HTML 解析 黄色(Scripting):JavaScript 执行 紫色(Rendering):样式计算和布局,即重排 绿色(Painting):重绘 灰色(other):其它事件花费的时间 白色(Idle):空闲时间
react perf devtools
class 组件的 shouldComponentUpdate
使用shouldComponentUpdate()
可以让 React 知道当前状态或属性的改变是否不影响组件的输出,默认返回 ture,返回 false 时不会重新渲染。
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
上述示例中只有当props.color
或者state.count
值发生变化时,组件才会重新渲染。
如果更复杂一些的组件,我们可以使用类似“浅比较”的模式来检查 props
和 state
中所有的字段,以此来决定是否组件需要更新。React 已经提供了一位好帮手来帮你实现这种常见的模式 - 你只要继承 React.PureComponent
就行了。所以这段代码可以改成以下这种更简洁的形式:
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = { count: 1 };
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState((state) => ({ count: state.count + 1 }))}
>
Count: {this.state.count}
</button>
);
}
}
减小视图大小
减小视图大小也是一个非常有用的优化手段。
比如我们有一个数据非常多的长列表,如果我们一次性渲染,肯定会性能非常差,但是如果我们只渲染看得见的部分,性能就得到了极大的提升。
下面举一个通过windowing的例子说明一下:
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => <div style={style}>Row {index}</div>;
const Example = () => (
<List height={150} itemCount={1000} itemSize={35} width={300}>
{Row}
</List>
);
在CodeSandBox上看一下运行效果。
这个长列表有 1000 个数据项,但是一次性只渲染 10 条数据项。这个列表的性能就会非常好。
对于不可见的 UI 部分,我们都可以采用延迟渲染的技巧来减少视图大小,提升性能。
状态更新合并
React 对状态更新做了一个优化:同时多次设置状态,不会引起多次重绘,而只会合并为一次重绘。当然这个优化是有前提的。我们来看两个例子。
例子 1:
import ReactDOM from 'react-dom';
import React, { useState } from 'react';
function Demo1() {
const [A, setA] = useState();
const [B, setB] = useState();
const handleClick = () => {
setA(1);
setB(2);
};
return (
<div>
<button onClick={handleClick}>更新</button>
<div>
{A} - {B}
</div>
</div>
);
}
ReactDOM.render(<Demo1 />, document.getElementById('root'));
点击例子 1 中的按钮,它会分别更新A
和B
两个状态,但是却只重绘了一次Demo1
组件:
再看看例子 2:
import ReactDOM from 'react-dom';
import React, { useState } from 'react';
function Demo2() {
const [A, setA] = useState();
const [B, setB] = useState();
const handleClick = () => {
setTimeout(() => {
setA(1);
setB(2);
});
};
return (
<div>
<button onClick={handleClick}>更新</button>
<div>
{A} - {B}
</div>
</div>
);
}
ReactDOM.render(<Demo2 />, document.getElementById('root'));
点击例子 2 中的按钮,你会发现Demo2
组件重绘了两次:
分析例子 2 与例子 1 的代码不同:
import ReactDOM from 'react-dom';
import React, { useState } from 'react';
function Demo2() {
const [A, setA] = useState();
const [B, setB] = useState();
const handleClick = () => {
+ setTimeout(() => {
setA(1);
setB(2);
+ });
};
return (
<div>
<button onClick={handleClick}>更新</button>
<div>
{A} - {B}
</div>
</div>
);
}
ReactDOM.render(<Demo2 />, document.getElementById('root'));
最重要的区别是:
setTimeout(() => {
setA(1);
setB(2);
});
在setTimeout()
中执行状态更新,每一次状态更新都会引起重绘,而不会合并为一次重绘。不仅仅是setTimeout()
,还包括setInterval()
、Promise
、web socket
、ajax
、Observable
等都是这样的。这是因为这些状态更新不是在 React Scheduler 中而是在其他环境中执行的。这里不深入展开对 React Scheduler 的分析,大家感兴趣的可以了解一下相关知识。
目前有两种方式解决:
方式一:使用useReducer
:
import ReactDOM from 'react-dom';
import React, { useReducer } from 'react';
function reducer(
state: { A?: number; B?: number },
action: { type: 'CHANGE' },
) {
switch (action.type) {
case 'CHANGE':
return {
A: 1,
B: 2,
};
default:
return state;
}
}
function Demo3() {
const [state, dispatch] = useReducer(reducer, {});
const handleClick = () => {
setTimeout(() => {
dispatch({ type: 'CHANGE' });
});
};
return (
<div>
<button onClick={handleClick}>更新</button>
<div>
{state.A} - {state.B}
</div>
</div>
);
}
ReactDOM.render(<Demo3 />, document.getElementById('root'));
方式二:使用ReactDOM.unstable_batchedUpdates()
:
import ReactDOM from 'react-dom';
import React, { useState } from 'react';
function Demo2() {
const [A, setA] = useState();
const [B, setB] = useState();
const handleClick = () => {
setTimeout(() => {
ReactDOM.unstable_batchedUpdates(() => {
setA(1);
setB(2);
});
});
};
return (
<div>
<button onClick={handleClick}>更新</button>
<div>
{A} - {B}
</div>
</div>
);
}
ReactDOM.render(<Demo2 />, document.getElementById('root'));
ReactDOM.unstable_batchedUpdates(fn)
会在 React Scheduler 上下文中执行fn
函数,所以setA(1)
和setB(2)
就会得到 React Scheduler 的优化,只会引发一次重绘。
但是ReactDOM.unstable_batchedUpdates()
API 还处于不稳定状态,而且是从ReactDOM
中引出来的,就会有React Native
的兼容性问题。建议使用import { batch } from 'react-redux';
中的batch
代替ReactDOM.unstable_batchedUpdates
。