React组件的性能优化

react组件的渲染性能优化

  • 单个组件的性能优化
  • 多个组件的性能优化
  • 利用reselect提高数据选取的性能
  • 动画效果
  • 代码打包
  • ...

1、单个组件的性能优化

React利用Virtual DOM来提高渲染性能,虽然每一次页面更新都是对组件的重新渲染,但是并不是将之前渲染的内容全部抛弃重来,借助Virtual DOM, React能够计算出对DOM树的最少修改,这就是React默认情况下渲染都很迅捷的秘诀。不过,虽然Virtual DOM能够将每次DOM操作量减少到最小,计算和比较Virtual DOM依然是一个复杂的计算过程。如果能够在开始计算Virtual DOM之前就可以判断渲染结果不会有变化,那样可以干脆不要进行Virtual DOM计算和比较,速度就会更快

安装插件React  Perf ,可以发现react组件渲染的性能问题

redux-immutable-state-invariant提供了redux的中间件,能够让redux在每次派发动作之后做一个检查,若某个reducer违反了作为一个纯函数擅自修改了state,给出警告⚠️

render函数决定了“组件渲染出什么”,shouldComponentUpdate决定“什么时候不需要重新渲染”

 

2、多个组件的性能优化

装载阶段基本没有什么选择,当一个React组件第一次出现在DOM树中时,无论如何是要彻底渲染一次的,从这个React组件往下的所有子组件,都要经历一遍React组件的装载生命周期,因为这部分的工作没有什么可以省略的,所以没有多少性能优化的事情可做。至于卸载阶段,只有一个生命周期函数componentWillUnmount,这个函数做的事情只是清理componentDidMount添加的事件处理监听等收尾工作,做的事情比装载过程要少很多,所以也没有什么可优化的空间。所以值得关注的过程,就只剩下了更新过程。

 React的调和(Reconciliation)过程

在装载过程中,React通过render方法在内存中产生了一个树形的结构,树上每一个节点代表一个React组件或者原生的DOM元素,这个树形结构就是所谓的Virtual DOM。React根据这个VirtualDOM来渲染产生浏览器中的DOM树。在装载过程结束后,用户就可以对网页进行交互,用户操作引发了界面的更新,网页中需要更新界面,React依然通过render方法获得一个新的树形结构Virtual DOM,这时候当然不能完全和装载过程一样直接用Virtual DOM去产生DOM树,不然就和最原始的字符串模板一个做法。而且,在真实的应用中,大部分网页内容的更新都是局部的小改动,如果每个改动都是推倒重来,那样每次都重新完全生成DOM树,性能肯定不可接受。实际上,React在更新阶段很巧妙地对比原有的Virtual DOM和新生成的Virtual DOM,找出两者的不同之处,根据不同来修改DOM树,这样只需做最小的必要改动。React在更新中这个“找不同”的过程,就叫做Reconciliation(调和)。

 

按照计算机科学目前的算法研究结果,对比两个N个节点的树形结构的算法,时间复杂度是O(N3),打个比方,假如两个树形结构上各有100节点,那么找出这两个树形结构差别的操作,需要100× 100×100次操作,也就是一百万次当量的操作,假如有一千个节点,那么需要相当于进行相当于1000×1000× 1000次操作,这是一亿次的操作当量,这么巨大数量的操作在强调快速反应的网页中是不可想象的,所以React不可能采用这样的算法。React实际采用的算法需要的时间复杂度是O(N),因为对比两个树形怎么着都要对比两个树形上的节点,似乎也不可能有比O(N)时间复杂度更低的算法。

 Key的用法

React不会使用一个O(N2)时间复杂度的算法去找出前后两列子组件的差别,默认情况下,在React的眼里,确定每一个组件在组件序列中的唯一标识就是它的位置,所以它也完全不懂哪些子组件实际上并没有改变,为了让React更加“聪明”,就需要开发者提供一点帮助。如果在代码中明确地告诉React每个组件的唯一标识,就可以帮助React在处理这个问题时聪明很多,告诉React每个组件“身份证号”的途径就是key属性。

庆幸的是,React会提醒开发者不要忘记使用key,同类型子组件出现多个实例时如果没有key的话,React在运行时会给出警告

理解了key属性的作用,也就知道,在一列子组件中,每个子组件的key值必须唯一,不然就没有帮助React区分各个组件的身份,这并不是一个很难的问题,一般很容易给每个组件找到一个唯一的id。但是这个key值只是唯一还不足够,这个key值还需要是稳定不变的,试想,如果key值虽然能够在每个时刻都唯一,但是变来变去,那么就会误导React做出错误判断,甚至导致错误的渲染结果。如果通过数组来产生一组子组件,一个常见的错误就是将元素在数组中的下标值作为key,下面的代码是错误的例子:

这么做非常危险,因为,假如没有使用key的话React会在运行时输出一个错误提示,但是错误地使用key值React就不会给出错误提示了,因为React无法发现开发者的错误。用数组下标作为key,看起来key值是唯一的,但是却不是稳定不变的,随着todos数组值的不同,同样一个TodoItem实例在不同的更新过程中在数组中的下标完全可能不同,把下标当做key就让React彻底乱套了。需要注意,虽然key是一个prop,但是接受key的组件并不能读取到key的值,因为key和ref是React保留的两个特殊prop,并没有预期让组件直接访问。

三、用reselect提高数据获取性能

reselect库的工作原理:只要相关状态没有改变,那就直接使用上一次的缓存结果。

reselect库被用来创造“选择器”,所谓选择器,就是接受一个state作为参数的函数,这个选择器函数返回的数据就是我们某个mapStateToProps需要的结果。

两阶段选择过程

在前面的章节,我们已经强调过React组件的渲染函数应该是一个纯函数,Redux中的reducer函数也应该是一个纯函数,mapStateToProps函数也应该是纯函数,纯函数让问题清晰而且简化。不过,现在这个“选择器”函数可不是纯函数,它是一种有“记忆力”的函数,运行选择器函数会有副作用,副作用就是能够根据以往的运行“记忆”返回“记忆”中的结果。reselect认为一个选择器的工作可以分为两个部分,把一个计算过程分为两个步骤:

步骤1,从输入参数state抽取第一层结果,将这第一层结果和之前抽取的第一层结果做比较,如果发现完全相同,就没有必要进行第二部分运算了,选择器直接把之前第二部分的运算结果返回就可以了。注意,这一部分做的“比较”,就是JavaScript的===操作符比较,如果第一层结果是对象的话,只有是同一对象才会被认为是相同。

步骤2,根据第一层结果计算出选择器需要返回的最终结果。显然,每次选择器函数被调用时,步骤一都会被执行,但步骤一的结果被用来判断是否可以使用缓存的结果,所以并不是每次都会调用步骤二的运算。选择器就是利用这种缓存结果的方式,避免了没有必要的运算浪费。剩下的事情就是确定选择器步骤一和步骤二分别进行什么运算。原则很简单,步骤一运算因为每次选择器都要使用,所以一定要快,运算要非常简单,最好就是一个映射运算,通常就只是从state参数中得到某个字段的引用就足够,把剩下来的重活累活都交给步骤二去做。

 

在TodoList这个具体例子中,todos和filter的值直接决定应该显示什么样的待办事项,所以,很显然步骤一是获取todos和filter的值,步骤二就是根据这两个值进行计算

使用reselect需要安装对应的npm包:

npm install --save reselect

在src/todos/selector.js文件中,选择器函数的代码如下:

reselect提供了创造选择器的createSelector函数,这是一个高阶函数,也就是接受函数为参数来产生一个新函数的函数。第一个参数是一个函数数组,每个元素代表了选择器步骤一需要做的映射计算,这里我们提供了两个函数getFilte和getTodos,对应代码如下:

上面说过,步骤一的运算要尽量简单快捷,所以往往一个Lambda表达式就足够。createSelector函数的第二个参数代表步骤二的计算过程,参数为第一个参数的输出结果,里面的逻辑和之前TodoList中的逻辑没有什么两样,只是这第二个函数不是每次都会被调用到。现在,我们可以在TodoList模块中改用新定义的选择器来获取待办事项数据了,代码如下:

Redux要求每个reducer不能修改state状态,如果要返回一个新的状态,就必须返回一个新的对象。如此一来,Redux Store状态树上某个节点如果没有改变,那么我们就有信心这个节点下数据没有改变,应用在reselect中,步骤一的运算就可以确定直接缓存运算结果。虽然reselect的createSelector创造的选择器并不是一个纯函数,但是createSelector接受的所有函数参数都是纯函数,虽然选择器有“记忆”这个副作用,但是只要输入参数state没有变化,产生的结果也就没有变化,表现得却类似于纯函数。只要Redux Store状态树上的filter和todos字段不变,无论怎样触发TodoList的渲染过程,都不会引发没有必要的遍历todos字段的运算,性能自然更快。虽然reselect库以re开头,但是逻辑上和React/Redux没有直接关系。实际上,在任何需要这种具有记忆的计算场合都可以使用reselect,不过,对于React和Redux组合的应用,reselect无疑能够提供绝佳的支持。

范式化状态树

所谓范式化,就是遵照关系型数据库的设计原则,减少冗余数据。如果读者做过关系型数据库(比如MySQL或者PostgreSQL)的表设计,就知道范式化的数据结构设计就是要让一份数据只存储一份,数据冗余造成的后果就是难以保证数据一致性。

与范式化相对,还存在“反范式化”的数据库设计,这在风生水起的NoSQL领域是一个惯常的设计风格。反范式化是利用数据冗余来换取读写效率,因为关系型数据库的强项虽然是保持一致,但是应用需要的数据形式往往是多个表join之后的结果,而join的过程往往耗时而且在分布式系统中难以应用。

 

假设我们给Todo应用做一个更大的改进,增加一个Type的概念,可以把某个TodoItem归为某一个Type,而且一个Type有特有的名称和颜色信息,在界面上,用户可以看到TodoItem显示为自己所属Type对应的颜色,TodoItem和Type当然是多对多的关系。用哪种方式设计状态树合适呢?更具体一点,如何设计代表TodoItem的状态树结构?如果使用反范式化的设计,那么状态树上的数据最好是能够不用计算拿来就能用,在Redux Store状态树的todos字段保存的是所有待办事项数据的数组,对于每个数组元素,反范式化的设计会是类似下面的对象:

在原有TodoItem对应状态中增加了type字段,包含name和color两个数据。当然,如果要达到数据扁平化的目的,应该是增加两个字段,分别为typeName和typeColor。这种状态设计的好处就是在渲染TodoItem组件时,从Redux Store上获得的状态可以直接使用name和color数据。但这也有缺点,当需要改变某种类型的名称和颜色时,不得不遍历所有TodoItem数据来完成改变。反范式化数据结构的特点就是读取容易,修改比较麻烦。如果使用范式化的数据结构设计,那么Redux Store上代表TodoItem的一条数据是类似下面的对象:

用一个typeId代表类型,然后在Redux Store上和todos平级的根节点位置创建一个types字段,内容是一个数组,每个数组元素代表一个类型,一个种类的数据是类似下面的对象:

当TodoItem组件要渲染内容时,从Redux Store状态树的todos字段下获取的数据是不够的,因为只有typeId。为了获得对应的种类名称和颜色,需要做一个类似关系型数据库的join操作,到状态树的types字段下去寻找对应typeId的种类数据。这个过程当然要花费一些时间,但是当要改变某个种类的名称或者颜色时,就异常地简单,只需要修改types中的一处数据就可以了。对比反范式方式和范式方式的优劣,不难看出范式方式更合理。因为虽然join数据需要花费计算时间,但是应用了reselect之后,大部分情况下都会命中缓存,实际上也就没有花费很多计算时间了。

总结

在本文中,我们了解了利用react-redux提供的shouldComponentUpdate实现来提高组件渲染功能的方法,一个要诀就是避免传递给其他组件的prop值是一个不同的对象,不然会造成无谓的重复渲染。通过了解React的Reconciliation过程,我们了解了React为什么能够在O(N)时间复杂度下完成VirutalDOM的比较,但是为了让React高效无误地完成工作,需要开发者做一些配合。首先,不能随意修改一个作为容器的HTML节点的类型。其次,对于动态数量的同类型子组件,一定要使用key这个prop。React的Reconciliation算法缺点是无法发现某个子树移动位置的情况,如果某个子树移动了位置,那React就会重新创建这个子树。当然,通常应用中不会出现这种情况。最后,我们学习了利用reselect库来实现高效的数据获取。因为reselect的缓存功能,开发者不用顾忌范式化的状态树会存在性能问题,Redux Store的状态树应该按照范式化原则来设计,减少数据冗余,这样利于保持数据一致。

 

Applies basic field behavior in circuit design and demonstrates how it relates to grounding and shielding requirements and techniques in circuit designThis book connects the fundamentals of electromagnetic theory to the problems of interference in all types of electronic design. The text covers power distribution in facilities, mixing of analog and digital circuitry, circuit board layout at high clock rates, and meeting radiation and susceptibility standards. The author examines the grounding and shielding requirements and techniques in circuit design and applies basic physics to circuit behavior. The sixth edition of this book has been updated with new material added throughout the chapters where appropriate. The presentation of the book has also been rearranged in order to reflect the current trends in the field.Grounding and Shielding: Circuits and Interference, Sixth Edition: Includes new material on vias and field control, capacitors as transmission lines, first energy sources, and high speed designs using boards with only two layersDemonstrates how circuit geometry controls performance from dc to gigahertzExamines the use of multi-shielded transformers in clean-power installationsProvides effective techniques for handling noise problems in analog and digital circuitsDiscusses how to use conductor geometry to improve performance, limit radiation, and reduce susceptibility to all types of hardware and systemsGrounding and Shielding: Circuits and Interference, Sixth Edition is an updated guide for circuit design engineers and technicians. It will also serve as a reference for engineers in the semiconductor device industry.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值