本文主要是针对性能优化做介绍,对应的React本身性能就比较不错!要介绍的点如下:
- 单个React组件的性能优化
- 多个React组件的性能优化
- 利用reselect提高数据选取的性能
因为现代化的Web应用的开发都是 组件化开发模式 ,因此我们不可能站在全局的角度去优化,而是针对Web应用的核心部分,也就是 组件 针对性的进行优化操作!
01|单个React组件性能优化
-
Virtual DOM帮助React组件提高渲染性能
- 虽然每次都是重新渲染,但是利用React最核心的功能,DIFF机制进行diff之后 渲染该渲染的部分!
- 虽然说每次的DOM操作量比较少,但是计算和比较的时候依然是一个比较复杂的过程! 这样一来就可以知道,在使用Virtual DOM之前就确认渲染结果不会有变化! 提升性能的方法不是diff而是不渲染甚至是不进行Diff!
-
对性能进行检测,对对应的部分进行针对性的优化!
-
避免过早优化,我们应该对性能影响最关键的代码进行优化!
-
使用提供的 shouldComponentUpdate 手动操作组件是否渲染! 更加细粒度的控制组件的渲染逻辑!
-
react-redux提供的 shouldComponentUpdate 对比prop和上一次使用到的prop上面,用的尽量简单的方法! 也只是对应的浅层比较,如果说对应的porp值是复杂的对象,只看是不是同一对象的引用,如果不是,哪怕这两个对象的内容完全一样,也会被认为两个不同的prop!
为什么不使用深层比较呢?
-
一个对象到底有多少层是无法预料的,如果使用递归对每个字段进行深层比较,让代码复杂的同时,也让性能比较低下!
-
对应的props如果说是对象类型的话, 想让react-redux认为前后的对象类型prop是相同的,就必须要保证只想的同一对象!
- 比如说是style属性 可以使用一个对象对样式进行设置,那么就可以将对应的 样式进行抽象成一个变量进行使用!
对应的 关于事件的处理,如果说每次都是以箭头函数的方式进行处理的话, 每次都是产生匿名函数! 匿名函数也就是每次都是新的函数 因此如果涉及到组件被应用到 多个页面中内存的占用就非常高!
对应的我们通过具体的代码可以很清楚的复现这个知识点:
import React,{PropTypes} from "react";
import {toogleTodo,removeTodo} from "../actions";
const TodoList = ({todos,onToggleTodo,onRemoveTodo})=>{
return (
<ul className='todo-list'>
todos.map((item)=>{
<TodoItem key={item.id} text={item.text} completed={item.completed}
onToggle={()=>onToogleTodo(item.id)}
onRemove={()=>onRemoveTodo(item.id)} />
})
</ul>
);
}
对应的TodoItem点击了对应的按钮,调用父类的回调函数,父类产生一个新的TodoItem 对应的每次更新都躲不过重新渲染的命运!
02|多个React组件的性能优化
多个React组件的优化,首先就需要学会从React的生命周期入手,从对应的生命周期阶段针对性的去优化!
render=>start: render method
op1=>operation: VDOM Tree
op2=>operation: RC or DOM
end=>end: View
action=>inputoutput: Action
render->op1->op2->end->action->op1
- render方法在内存中产生了树形的结构(Virtual DOM)
- 树上的每一个节点表示: React组件或者说是原生的DOM元素
- React 根据 Virtual DOM渲染浏览器中的DOM树!
如果说用户的交互出发了页面的更新,网页中需要更新页面的话,React 依然通过render方法获得了一个树形结构VirtualDOM 这个时候不能完全和装载过程一样直接使用VirtualDOm去产生DOM树!
-
这个阶段(更新阶段)巧妙的对比原有的Virtual DOM和新生成的Virtual DOM找出两者的不同之处,根据不同来更新DOM! 只做必要的最小的改动!
-
对应的这个找不同的过程叫做 Reconciliation 调和!
对照计算机科学目前的算法研究成果,比对N个节点的树形结构的算法,时间复杂度是 如果说比对两个100节点的DOM树需要计算 次 其中的算法复杂度 N表示节点数,
但是对应的React实际采用的算法需要的时间复杂度为 ,对比两个树形怎么着都要比对两个树形上的节点!
也不存在比复杂度更低的算法
对应的调和过程:
- 节点类型不同的情况
- React会直接丢弃原来树形结构,然后重建DOM树,对应的React组件也会经历卸载的生命周期
- 面对不同的应用场景,可能会引发树结构某些组件的卸载和装载过程!
- 节点类型相同的情况
- 如果两个数形结构的根节点类型相同,React就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载!
- 对应的节点类型分为两种:一种是DOM元素对应的也就是所谓的HTML元素,另一种则是 React的组件,也就是利用React库定制的类型!
- 如果两个数形结构的根节点类型相同,React就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载!
对应的如果说属性结构的根节点不是DOM元素,那就只可能是React组件类型,那么React做的工作类似,React此时也不知道如何更新DOM树,因此逻辑还在React组件之中,React能做的也就是通过新节点的props更新原来的组件实例,引发组件实例更新的过程! 按照顺序触发:
- shouldComponentUpdate 如果不需要更新的话,可以在函数中直接返回false来保持最大的性能!
- componentWillReceiveProps
- componentWillUpdate
- render
- componentDidUpdate
01|如果说多个子组件的情况是怎么样的呢?
我们那一个最简单的TodoItem来举例吧:
<ul>
<TodoItem text="first" completed={false} />
<TodoItem text="second" completed={false} />
</ul>
在更新之后,用JSX表示是这样:
<ul>
<TodoItem text="first" completed={false} />
<TodoItem text="second" completed={false} />
<TodoItem text="Thrid" completed={false} />
</ul>
对应的结果就是,react检查多出了一个TodoItem,创建一个新的TodoItem组件实例,该实例需要经历装载的过程,但是对于前面两个TodoItem实例,React会引发他们的更新过程! 但是如果说对应的shouldComponentUpdate函数实现恰当,props检查之后就返回false之后,可以避免实质的更新操作!
- 刚刚那样是在后面加了一个TodoItem实例,如果说在前面加的话又会出现什么问题呢?
<ul>
<TodoItem text="zero" completed={false} />
<TodoItem text="first" completed={false} />
<TodoItem text="second" completed={false} />
</ul>
和之前的代码实例相比,此时的React会这么处理:
- 首先认为把text从first改为了zero
- second改为了first
- 最后多出了一个TodoItem实例内容为second
现存(first,second)的两个实例的属性被改变了,强迫他们完成了一个更新! 虽然这种情况只是改变了2个组件的属性,如果说有一百个TodoItem实例的话,明显就是一个浪费!
后面React提供了方法来克服这种浪费,于是有了key
02|Key的用法
React中,需要确定每一个组件在组件序列中的唯一标识就是它的位置! 因此React本身也不懂哪些子组件实质上面没有改变! key就是每一个组件的唯一标识!
<ul>
<TodoItem key={1} text="zero" completed={false} />
<TodoItem key={2} text="first" completed={false} />
<TodoItem key={3} text="second" completed={false} />
</ul>
- key的值需要保证唯一性
- 通过key的使用配合shouldComponentUpdate就能够一定程度上面提高性能!
03|使用reselect提高数据获取性能
对应的除了通过优化渲染过程来提高性能,既然React和Redux都是通过数据驱动渲染过程,除了渲染过程,获取数据的过程也是一个需要考虑的优化点!
通过mapStateToProps函数从Redux store提供的state中产生渲染需要的数据,对应的代码如下所示:
const selectVisibleTodos = (todo,filter)=>{
switch(filter){
case FilterType.ALL:
return todos;
case FilterType.COMPLETED:
return todos.filter(item=>item.completed);
case FilterTypes.UNCOMPPLETED:
return todos.filter(item=>!item.completed);
default:
throw new Error("unsupport filter!");
}
}
const mapStateToProps = state=>{
return {todos:selectVisibleTodos(state.todos,state.filter)};
}
Redux Store上获取数据的重要一环,mapStateToProps函数一定要快,从代码来看,运算本身没有什么课优化的空间,
获取对应的待办事项,需要通过对应的todos和filter两个字段的值计算出来! 计算过程需要遍历todos字段上的数组,数组比较大的时候,TodoList组件的每一次重新渲染都需要重新计算一遍,负担就会过重!
- 两阶段选择过程
对应的selectVisibleTodos函数的计算必不可少,那么对应的如何优化呢?
并不是每一次TodoList的渲染都需要执行selectVisibleTodos中的计算过程,如果对应的Redux Store状态树上的待办事项的todos字段没有变化,而代表当前过滤器的filter字段也没有变化,实在没有必要重新渲染todos数组来计算一个新的结果! 如果说上一次的结果能够被缓存过来的话,那么就重用缓存就行了!
reselect库的工作原理就是,只要相关状态没有改变的话,那就直接重用上一次的缓存!
reselect库被用来创造 选择器:接受一个state作为参数,并且通过选择器返回的函数的数据就是我们某个mapStateToProps需要的结果! 但是选择器不是纯函数,一种有记忆力的函数,运行选择器函数会有副作用!
- 通过输入参数state抽取第一层结果,降低一层结果与之前的结果进行比对,如果完全相同没必要进行比对,这一部分的比较,就是JavaScript中的全等操作符比较! 如果是对象且是同一个对象才会被认为是相同! 否则进入到下一步
- 接下来的就是确定选择器步骤一和步骤二分别进行什么计算,原则很简单:
- 步骤一:尽量快,运算非常简单的,最好就是一个映射运算 通常是state参数中某个字段的引用!
- 之后的活交给第二步去计算!
对上面的代码进行改造就需要使用 reselect
库:
import {createSelector} from "reselect";
import {FilterTypes} from "../constants.js";
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("unsupport filter!");
}
})
const getFilter = state=> state.filter;
const getTodos = state=> state.todos;
import {selectVisibleTodos} from "../selector.js";
const mapStateToProps = state=>{
return {todos:selectVisibleTodos(state)};
}
这样一来虽然说createSelector接受的所有函数都是为纯函数,但是选择器有记忆的副作用,只要对应的state没有变化自然输出也没有变化!
只要是Redux store状态树上的filter和todos字段不变的话,怎么触发TodoList的渲染过程,都不会触发遍历todos字段的计算,性能自然更快!
- 状态树的设计尽量的范式化,按照一定的设计规约he关系型数据库的设计原则,减少数据的冗余!(数据冗余造成的后果就是难以保证数据的一致性!)
网友评论