性能优化
基于 Webpack 等构建工具打包带来的性能优化本章略过,主要讨论 React 内部的针对性优化
虚拟滚动
参考库
虚拟滚动参考react-window和react-virtualized两个热门库
原理
虚拟列表指的是「可视区域渲染」的列表。有三个概念需要了解:
- 滚动容器元素:一般情况下,滚动容器元素是 window 对象,也可以通过布局的方式,在某页面内指定一个或多个滚动容器元素,滚动也分横向和纵向滚动,滚动容器元素在滚动时每个列表项只是渲染一些纯文本。在这里我们只讨论元素纵向滚动
- 可滚动区域:滚动容器元素的内部内容区域。假设有 100 条数据,每个列表项的高度是 50,那么可滚动的区域高度就是 100*50。可滚动区域当前具体高度值一般通过滚动容器元素的 scrollHeight 属性获取
- 可视区域:滚动容器元素的视觉可见区域。如果容器元素是 window 对象,可视区域就是浏览器的视口大小(即视觉视口);如果某个容器元素 div,其高度 300,右侧有纵向滚动条,那么视觉可见的区域就是可视区域
虚拟滚动的实现是在处理用户滚动时,改变列表在可视区域的渲染部分,其具体步骤如下
- 计算当前可见区域起始数据的 startIndex
- 计算当前可见区域结束数据的 endIndex
- 计算当前可见区域的数据,并渲染到页面中
- 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上
- 计算 endIndex 对应的数据相对于可滚动区域最底部的偏移位置 endOffset,并设置到列表上
参考链接:https://blog.csdn.net/terrychinaz/article/details/112552673
rerender 优化

在这张组件树图例中,C6 触发的更新引起组件树重新渲染(rerender),如果避开无需重渲染的组件:C2 及其子组件、C3 的子组件 C7、C8,最终执行最小量的重渲染组件就只会有 C1-C3-C6,从而提高渲染效率优化加载性能
shouldComponentUpdate
假设场景:组件只有当props.color
或state.count
的值改变才需要更新,可以使用 shouldComponentUpdate 来检查
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>
);
}
}
如果 color 和 count 的值没有改变,则组件不会触发 rerender
PureComponent
仅对 props 和 state 中的所有字段作浅比较的情况,使用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>
);
}
}
memo
memo 是 React 面向函数式组件提供的浅比较 API,作用与PureComponent
相同,包裹函数组件来阻止函数组件不必要的更新
export default memo(function CounterButton(props) {
const [count, setCount] = useState(1);
return (
<button color={props.color} onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
});
useMemo
详见惰性取值Hook
useCallback
详见惰性函数Hook
不可变数据操作
众所周知,React 遵守组件状态不可变的理念,通常在修改 state 的时候都需要使用 ES6 的解构,或者新对象赋值
immer.js 为我们提供了全新的无须使用解构即可触发 React 更新的写法
传统写法:
const App = () => {
const [state, setState] = useState({
name: "lin",
todoList: {
list: [{ name: "吃饭", done: true }],
},
});
const addTodoList = () => {
setState((state) => ({
name: state.name,
todoList: {
list: [...state.todoList.list, { name: "睡觉", done: true }],
},
}));
};
};
使用immer
:
import { produce } from "immer";
const App = () => {
const [state, setState] = useState({
name: "lin",
todoList: {
list: [{ name: "吃饭", done: true }],
},
});
const addTodoList = () => {
setState(
produce((state) => {
state.todoList.list.push({ name: "睡觉", done: true });
})
);
};
};
immer 实现原理
immer 会通过原有的 state 基础状态生成一个可编辑的 draft 状态,开发者修改数据,修改完成后 immer 只会针对数据有变化的部分进行深拷贝,然后返回一个新状态,整个过程不影响初始状态
- draft 的实现
draft 本质是初始状态的代理,核心是为了不让开发者直接触及到原始状态,而是在其代理上进行修改,再根据代理状态的变化来生成新状态
immer 中对于 draft 的实现在支持 Proxy 语法环境时使用 Proxy 实现,不支持 Proxy 语法环境时使用 defineProperty 实现
为避免内存泄露并保证安全性,immer 还使用Proxy.revocable
创建了可撤销的代理对象,draft 函数执行完成后调用 revoke 方法销毁 draft 对象
按需深拷贝的实现
immer 中通过对代理状态的劫持来实现按需标记更改
当在 produce 的回调函数(recipe 函数)中修改 draft 数据时,就会通过 set 或 deleteProperty 中的 markChange 完成变化追踪,同时通过
state.copy_
来记录修改后的值,并通过state.assigned_
记录修改的类型数据修改完成后,通过
state.copy_
和state.modified_
生成新的状态上述核心思想就是通过递归遍历子属性,用之前的标记判断子属性部分的数据是否有被修改过,如果未被修改则直接使用原始状态中的引用,否则就用
state.copy_
记录的更新内容来重设子属性数据
参考链接:http://lixianglong.cn/2022/02/25/application/fore-end/nodejs/immer.js 入门/