一、单个 React 组件的性能优化
- chrome 插件 React Pref
- render: ‘组件渲染出什么’
- shouldComponentUpdate:‘什么时候不需要重新渲染’
- 善于使用 shouldComponentUpdate
二、多个 React 组件的性能优化
- React 的调和(Reconciliation)过程
- 找出原有的 Virtual DOM 和新生成的 Virtual DOM
- React 在更新中这个‘找不同’的过程,就叫做调和(Reconciliation)
- 对比两个 N 个节点的属性结构的算法,时间复杂度是O(N^3)
- React 实际采用的算法需要的时间复杂度是 O(N)-----性能和复杂度的最好折中
- 节点类型不同的情况—意味着改动太大
- 原有的树形上的 React 组件会经历“卸载”的生命周期 (componentWillUnmount)
- 取而代之的组件会经历装载过程的生命周期(componentWillMount、render、componentDidMount 依次调用)
- 节点类型相同的情况
- 原来的跟节点只需要更新过程
- 节点类型两类: DOM 元素类型(HTML 直接支持的元素类型)、React 组件
- DOM 元素:只修改发生变化的部分,让 DOM 操作尽可能少
- React 组件类型:更新过程: componentWillReceiveProps -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate
- 重视 shouldComponentUpdate, 如果发现没必要重新渲染直接返回 false
- 节点类型不同的情况—意味着改动太大
- Key 的用法
- 告诉 React 每个组件的唯一标识(key 属性)
- key 值不止唯一,还需要稳定不变
- 用数组下标作为 key ,看起来唯一,但是却不是稳定不变的
三、利用 reselect 提高数据选取的性能
- reselect 库的工作原理: 只要相关状态没有改变,那就直接使用上一次的缓存结果
- 用来创造“选择器”:就是接受一个 state 作为参数的函数,这个选择器函数返回的数据就是我们某个 mapStateToProps 需要的结果
- 两个步骤
- 1、从输入参数 state 抽取第一层结果,将这第一层结果和之前抽取的第一层结果做比较,如果发现完全相同,就没必要进行第二部分运算了,选择器直接吧之前第二部分的运算结果返回就可以 (注:这一部分做的 “比较”就是 Javascript 的 === 操作符比较,如果第一层结果是对象的话,只有是同一个对象才会被认为是相同的)
- 2、根据第一层结果计算出选择器需要返回的最终结果
- 步骤一都会被执行,根据步骤一的结果来判断是否可以使用缓存的结果,是否需要执行第二步。
- 使用方式:
- npm install --save reselect
- 以 todos 为例:
import { createSelector } from 'reselect'
import { FilterTypes } from '../constants.js'
export const selectVisibleTodos = createSelector(
[getFilter, getTodos],
(filter, todos) => {
switch (filters) {
case FilterTypes.All:
return todos
case FilterTypes.COMPLETED:
return todos.filter(item => item.completed)
case FilterTypes.UNCOMPLETED:
return todos.filter(item => !item.completed)
default:
throw new Error('unsupported filter')
}
}
)
const getFilter = (state) => state.filter
const getTodos = (state) => state.todos
- 第一个参数是一个函数数组,每个元素代表了选择器步骤一需要做的映射计算,这里我们提供了两个函数 getFilter、getTodos
- 第二个参数代表步骤二的计算过程,参数为第一个参数的输出结果
- 虽然 reselect 的 createSelector 创造的焉择其并不是一个纯函数,但是 createSelector 接受的搜有函数参数都是纯函数,虽然选择器有“记忆”这个副作用,到那时只要输入参数 state 没有变化,产生的结果也就没有变化,表现的却类似于纯函数
- 例如例子里的,只要 Redux Store 状态树上的 filter 和 todos 字段不变,无论怎样触发 TodoList 的渲染过程,都不会引发没有必要的遍历 todos 字段的运算,性能自然更快
- 范式化的状态树
- 范式化: 遵照关系型数据库的设计原则,减少冗余数据
- 反范式化:利用数据冗余换取读写效率
- 例子:给 Todo 增加一个 Type 概念
// 反范式化
{
id: 1, // 待办事项 id
text: '待办事项1', // 待办事项文字内容
completed: false, // 是否已完成
type: { // 种类
name: '紧急', // 种类名称
color: 'red' // 种类显示颜色
}
}
- 反范式化
- 优点: 从 Redux Store 上获得的状态可以直接使用name 和 color 数据
- 缺点:需要改变某种类型的名称和颜色是,需要遍历所有的 TodoItem来完成
- 特点: 读取容易,修改麻烦
// 范式化
{
id: 1, // 待办事项 id
text: '待办事项1', // 待办事项文字内容
completed: false, // 是否已完成
typeId: 1 // 待办事项所属的种类 id
}
{
id: 1, // 种类 id
name: '紧急', // 种类名称
color: 'red' // 种类显示颜色
}
- 范式化
- 优点: 修改某个种类的名称或者颜色,只需要修改types 中的一处数据就行
- 缺点: 获取数据时,需要做一个类似关系型数据库的 join 操作
- 两者对比,范式方式更合理,虽然 join 数据需要花时间,但是应用了 reselect 之后,大部分情况都会命中缓存,实际上也就没有花费很多计算时间了。
总结:
- 避免传递给其他组件的prop 值是一个不同的对象,不然会造成无谓的重复渲染
- 不能随意修改一个作为容器的 HTML 节点类型
- 对于动态数量的同类型子组件,一定要使用 key 这个 prop
- React 的 Reconciliation 算法缺点是无法发现某个子树移动位置的情况,如果某个子树移动了位置,那 React 就会重新创建这个子树。
- 利用 reselect 库来实现高效的数据获取。Redux Store 的状态树应该按照范式化原则来设计,减少数据冗余,这样利于保持数据一致。
该文章是对《深入浅出 React 和 Redux 》一书中的第五章