单个 React 组件的性能优化
对比prop和上一次渲染所用的 prop,使用的是“浅层比较”(===),如果 prop 的类型是字符串或者数字,只要值相同,那么 “浅层比较” 也会认为二者相同,但是如果 prop 的类型是复杂对象, 那么浅层比较的方式只看两者是否是同一对象的引用,如果不是就就算两个对象中的内容完全一样,也会被认为是两个不同的 prop
1.在给组件 style 的prop 赋值时
//错误
<Foo style={{coloe:"red"}}/>
使用这个方法每一次渲染都会认为这个style发生了变化,因为每次都会产生一个新的对象给 style,在“浅层比较”中只会比较第一层,不会比较内容是否相等。
//正确
const fooStyle = {coloe:"red"};
<Foo style={fooStyle }/>
2. 在给组件 onClick 的prop 赋值时,
//错误
onClick={() => this.handleClick()}
这里赋值的是一个匿名函数,在赋值的时候产生的,每次渲染都会产生一个新的函数,即使函数的内容一样。
//正确
constructor() {
this.handleClick = this.handleClick.bind(this);
}
handleClick() {}
onClick={handleClick}
多个 React 组件的性能优化
React 的调和(Reconciliation )过程
React 在更新阶段很巧妙的对比原有的 Virtual DOM和新生成的 Virtual DOM,找出两者的不同之处,根据不同来修改 DOM 树,React 在更新中找不同的过程,就叫做调和。
1.节点类型不同的情况
如果树形结构的根节点不相同,会直接认为原来的树形结构没用,需要重新构建新的 DOM 树,原有的树形上的 React 组件会经历 “卸载” 的生命周期。
//更新前
<div>
<Todo />
<div/>
//更新后
<span>
<Todo />
</span>
这时候, componentWillUnmount 方法会被调用,取而代之的组件则会经历装载过程
的生命周期,组件的 componentWillMount render componentDidMount 方法依次被
调用,一看根节点原来是 div ,新的根节点是 span ,类型就不一样,切推倒重
来。
虽然是浪费,但是为了避免 O(N3)的时间复杂度, React 必须要选择 个更简单更快
捷的算法,也就只能采用这种方式
作为开发者,很显然一定要避免上面这样浪费的情景出现 所以, 一定要避免作为
包裹功能的节点类型被随意改变
2.节点类型相同
如果两个树形结构的根节点类型相同,React 就认为原来的根节点只需要更新过程,不会将其写在,也不会引发根节点重装。
如果属性结构的根节点不是 DOM 元素类型,就是组件类型,会根据新节点的 props 去跟新原来根节点的实例,也就是按照顺序引发下列函数:
- shouldComponentUpdate
- componentWillReceiveProps
- componentWillUpdate
- render
- componentDidUpdate
如果 shouldComponentUpdate 函数返回 false 的话,那么更新过程
就此打住,不再继续 所以为了保持最大的性能,每个 React 组件类必须要重视 shouldComponentUpdate,如果发现根本没有必要重新渲染,那就可以直接返回 false
3.多个子组件的情况
如果多个子组件是由相同组件创造的实例,就必须使用 key 来区分实例的不同。
key的用法
必须使用一个稳定值,比如id。不能使用数组下标作为 key ,因为看起来 key 值是唯一的,但是却不是稳定不变的, 需要注意,虽然 key 是一个 prop ,但是接受 key 的组件并不能读取到 key 的值,key 和 ref 是 React 保留的两个特殊 prop ,并没有预期让组件直接访问。
用reselect 提高数据获取性能
在有的组件中需要根据传入的参数对数组的数据进行过滤,获取到想要的数据。每次切换不同值,就算数组数据没有改变也会根据参数重新计算,这样就造成了浪费。例如:
const filtrationTodos = (todos, filter) => {
console.log(todos, filter);
switch (filter) {
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('错误')
}
};
TodoList.propTypes = {
todos: PropTypes.array.isRequired
};
//获取todos数组将todos映射到props
const mapStateToProps = (state) => {
return {
todos: filtrationTodos(state.todos, state.filter)
}
};
可以使用reselect 进行优化。reselect 库的工作原理:只要相关状态没有改变,那就直接使用上一次的缓存结果。
reselect 库被用来曾创造“选择器”,所谓选择器就是接受一个 state 作为参数的函数,这个选择器函数返回的数据就是某个 mapStateToProps 需要的结果。
一个选择器的工作可以分为两个部分,把一个计算结果分为两个步骤:
步骤一:从输入参数 state 抽取第一层结果结果,将第一层结果和之前抽取的第一层结果做比较,如果发现完全相同,就不做第二部分的运算,选择器直接把之前第二部分分的运算结果返回。(这部分比较使用===操作符,如果是对象必须是同一对象)步骤一运算由于每次都要调用,所以要快最好是一个映射运算,通常就是从state参数中得到某个字段的引用,剩下的交给步骤二做。
步骤二:根据第一层结果计算出选择器需要返回的最终结果。具体实现:
import {createSelector} from 'reselect';
import {FilterTypes} from '../constants';
const getFilter = (state) => state.filter;
const getTodos = (state) => state.todos;
export const selectVisibleTodos = createSelector(
[getFilter, getTodos],
(filter, todos) => {
switch (filter) {
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('错误')
}
}
);
reselect 提供了创造选择器的 createSelector 函数 ,这是一个高阶函数,也就是接受函数为参数来产生一个新函数的函数.。
第一个参数是一个函数数组,每个元素代表了选择器步骤一需要做的映射计算,这
里我们提供了两个函数 getFilte和 getTodos
使用
//获取todos数组将todos映射到props
const mapStateToProps = (state) => {
return {
todos: selectVisibleTodos(state)
// todos: filtrationTodos(state.todos, state.filter)
}
};
范式化状态树
所谓范式化,就是遵照关系型数据库的原则,减少冗余数据
反范式化的设计,那么状态树上的数据最好是能够不用计算拿来就能用。反范式化数据结构的特点就是读取容易,修改比较麻烦。
假设我们给Todo 应用做一个更大的改进,增加一个 Type 的概念,可以把某个 TodoItem 归为某一个 Type,而且一个 Type有特有的名称和颜色信息,在界面上用户可以看到 TodoItem 显示为自己所属 Type 对应的颜色, TodoItem 和 Type是多对多关系。
反范式设计:
{
id: 1 ,
text :”待办事项 l ”,
completed : false ,
type: { //种类
name :”紧急”,//种类的名称
color:”red” //种类的显示颜色
}
}
这种设计的优点在于渲染 TodoItem 组件时,从 Redux Store 上获取的状态可以直接使用 name 和color 数据。缺点在于,当需要概念某种类型的名称和颜色时,不得不遍历所有 TodoItem 数据来完成改变。
范式设计
Redux Store 上代表 TodoItem 的一条数据就是类似下面的对象
{
id: 1, //待办事项id
text: “待办事项1”, //代办事项文字内容
completed: false, //代办事项是否完成
typeId: 1 //代办事项所属种类id
}
用一个 typeId 代表类型, 然后在 Redux Store 上和 todos 平级的根节点位置创建一个 types字段,内容是一个数组,每个数组元素代表一个类型,一个种类的数据是类似下面的对象:
{
id: 1, //种类id
name: "紧急", //种类名称
color: “red” //种类显示颜色
}
当 TodoItem 组件要渲染内容时,从 Redux Store 状态树的 to dos 宇段下获取的数据是不够的,因为只有 typeId。为了获得对应的种类名称和颜色,需要做一个类似关系型数据库的 join 操作,到状态树的 typoes 字段下去寻找对应 typeId 的种类颜色。
这个过程当然要花费一 些时间,但是当要改变某个种类的名称或者颜色时,就异常
地简单,只需要修改 types 中的一处数据就可以了
小结
1.利用 react-redux 提供的 shouldComponentUpdate 实现来提高
组件渲染功能的方法, 一个要诀就是避免传递给其他组件的 prop 值是 一个不同的对象,
不然会造成元谓的重复渲染
2.不能随意修改一个作为容器的 HTML 节点的类型 其次,对于动态数
量的同类型子组件,一 定要使用 key 这个 prop
3.利用 reselect 库来实现高效的数据获取。 因为 reselect 的缓存功
能,开发者不用顾忌范式化的状态树会存在性能问题, Redux Store 的状态树应该按照范
式化原则来设计,减少数据冗余,这样利于保持数据一致。
网友评论