乐趣区

锵哥带你读好书系列之深入浅出React和Redux第五章React组件的性能优化

风说:虽然我没有颜色,但我很自由

剧情回顾:

昨天我们已经学习了如何从一个应用维度去思考一个 React 应用的构思与设计,这是从 0 到 1 的过程,接着我们要开始从 1 到 100 的提升过程。
今天给大家介绍一下,React 组件是如何性能优化的,毕竟写出来跟写好是两种不同的境界。

正文:

章节:《深入浅出 React 和 Redux》(第五章:React 组件的性能优化)

1. 本章会介绍性能优化方法:
A: 单个 React 组件的性能优化
B: 多个 React 组件的性能优化
C: 利用 reselect 提高数据选取的性能

2.React 利用 Virtual DOM 来提高渲染性能,虽然每一次页面更新都是对组件的重新渲染,但是并不是将之前渲染的内容全部抛弃重来,借助 Virtual DOM,React 能够计算出对 DOM 树的最小修改,这就是 React 默认情况下渲染都很迅捷的秘诀

3.React Pref 的使用方法,请查看书本 106 页,主要就是
A: 打开开发者工具,切换到 Pref,确保 wasted 选项被勾选
B: 点击 React Pref 工具左侧的 Start 按钮,开始性能测量,这时按钮会变成 Stop 按钮
C: 然后勾选 Todo 应用的 First 那一项,使它变成完成状态
D: 点击 React Pref 工具左侧的 Strop 按钮,结束性能测量,界面上会显示测量结果。

4. 在 React Pref 工具中,可以看见发现浪费的渲染过程,React Pref 工具记录在点击 Start 按钮和 Stop 按钮之间的所有的 React 渲染,如果有组件计算 Virtual DOM 之后发现和之前的 Virtual DOM 相同,那就认为是一次浪费。注意,这里说的浪费是计算 Virtual DOM 的浪费,并不是访问 DOM 树的浪费

5. 我们应该忘记忽略很小的性能优化,可以说 97% 的情况下,过早的优化是万恶之源,而我们应该关心对性能影响最关键的那另外 3% 的代码

6. 所谓的“过早的优化”,指的是没有任何量化证据情况下开发者对性能优化的猜测,没有可测量的性能指标,就完全不知道当前的性能瓶颈在何处,完成优化之后也无法知道性能优化是否达到了期望的目标

7. 虽然代码上不可见,但是 connect 的过程中实际上产生了一个无名的 React 组件类,这个类定制了 shouldComponentUpdate 函数的实现

8. 只要 Redux Store 上对应 state 没有改变,Foo 就不会经历无意义的 Virtual DOM 产生和比对过程,也就避免了浪费

9. 同样的方法也可以应用在 TodoItem 组件上。不过因为 TodoItem 没有直接从 Redux Store 上读取状态,但我们依然可以使用 react-redux 方法,只是 connect 函数的调用不需要任何参数,要做的只是将定义 TodoItem 组件的代码最后一行改成如下代码,代码如下:
export default connect()(TodoItem);

10. 在上面的例子中,在 connect 函数的调用没有参数,没有 mapStateToProps 和 mapDispatchToProps 函数,使用 connect 来包裹 TodoItem 的唯一目的就是利用那个聪明的 shouldComponentUpdate 函数

11. 总之,要想让 react-redux 认为前后的对象类型 prop 是相同的,就必须要保证 prop 是指向同一个 JavaScript 对象

12. 同样的情况也存在于函数类型的 prop,react-redux 无从知道两个不同的函数是不是做着一样的事情,要想让它认为两个 prop 是相同的,就必须让这两个 prop 指向同样一个函数,如果每次传入 prop 的都是一个新创建的函数,那肯定就没法让 prop 指向同一个函数了

13. 对比两种方式,可以看到无论如何 TodoItem 都需要使用 react-redux,都需要定义产生定制 prop 的 mapDispatchToProps,都要求 TodoList 传入一个 id,区别只在于 actions 是由父组件导入还是由组件自己导入。

14. 我们现在考虑的不是单个 React 组件内的渲染过程,而是多个 React 组件之间组合的渲染过程。和单个 React 组件的生命周期一样,React 组件也要考虑三个阶段:装载阶段、更新阶段和卸载阶段

15. 在装载过程中,React 通过 render 方法在内存中产生了一个树形的结构,树上每一个节点代表一个 React 组件或者原生的 DOM 元素,这个树形结构就是所谓的 Virtual DOM,React 根据这个 Virtual DOM 来渲染产生浏览器中的 DOM 树

16. 实际上,React 在更新阶段很巧妙地对比原有的 Virtual DOM 和新生成的 Virtual DOM,找出两者的不同之处,根据不同来修改 DOM 树,这样只需做最小的必要改动。React 在更新中这个“找不同”的过程,就叫做 Reconciliation(调和)

17.React 实际采用的算法需要的时间复杂度是 O(N),因为对比两个树形怎么着都要对比两个树形上的节点,似乎也不可能有比 O(N)时间复杂度更低的算法。

18. 其实 React 的 Reconciliation 算法并不复杂,当 React 要对比两个 Virtual DOM 的树形结构的时候,从根节点开始递归往下比对,在树形结构上,每个节点都可以看作一个这个节点以下部分子树的根节点。所以其实这个对比算法可以从 Virtual DOM 上任何一个节点开始执行。

19. 如果树形结构根节点类型不相同,那就意味着改动太大了,也不要去费心考虑是不是原来那个树形的根节点被移动到其他地方去了,直接认为原来那个树形结构已经没用,可以扔掉,需要重新构建新的 DOM 树,原来的树形上的 React 组件会经理“卸载”的生命周期

20. 虽然是浪费,但是为了避免 O(N³)的时间复杂度,React 必须要选择一个更简单更快捷的算法,也就只能采用这种方式。

21. 作为开发者,很显然一定要避免上面这样浪费的情景出现。所以,一定要避免作为包裹功能的节点类型被随意改变,像上面的例子中,把 div 换成 span 只会带来没有必要的组件重新装载

22. 如果两个树形结构的根节点类型相同,React 就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载。

23. 这时,有必要区分一下节点的类型,节点的类型可以分为两类:一类是 DOM 元素类型,对应的就是 HTML 直接支持的元素类型,比如 div,span 和 p;另一类是 React 组件,也就是利用 React 库定制的类型。

24.React 不会使用一个 O(N²)时间复杂度的算法去找出前后两列子组件的差别,默认情况下,在 Reat 的眼里,确定每一个组件在组件序列中的唯一标识就是它的位置,所以他也完全不懂哪些子组件实际上并没有改变,为了让 React 更加“聪明”,就需要开发者 提供一点帮助

25. 如果在代码中明确地告诉 React 每个组件的唯一标识,就可以帮助 React 在处理这个问题时聪明很多,告诉 React 每个组件“身份证”的途径就是 key 属性。

26. 理解了 key 属性的作用,也就知道,在一列子组件中,每个子组件的 key 值必须唯一,不然就没有帮助 React 区分各个组件的身份,这并不是一个很难的问题,一般很容易给每个组件找到一个唯一的 id。

27. 但是这个 key 值只是唯一还不足够,这个 key 值还需要是稳定不变的,试想,如果 key 值虽然能够在每个时刻都唯一,但是变来变去,那么就会误导 React 做出错误判断,甚至导致错误的渲染结果。

28. 如果通过数组来产生一组子组件,一个常见的错误就是将元素在数组中的下标值作为 key,这么做非常危险,因为,假如没有使用 key 的话 React 会在运行时输出一个错误提示,但是错误地使用 key 值 React 就不会给出错误提示了,因为 React 无法发现开发者的错误。

29 用数组下标作为 key,看起来 key 值是唯一的,但是却不是稳定不变的,随着 todos 数组值的不同,同样一个 TodoItem 实例在不同的更新过程中在数组中的下标完全可能不同,把下标当做 key 就让 React 彻底乱套了。

30. 在前面的例子中,都是通过优化渲染过程来提高性能,既然 React 和 Redux 都是通过数据驱动渲染过程,那么除了优化渲染过程,获取数据的过程也是一个需要考虑的优化点。可以使用 reselect 库来提高数据获取性能。

31.reselect 提供了创造选择器的 createSelector 函数,这是一个高阶函数,也就是接受函数为参数来产生一个新函数的函数。

32.Redux 要求每一个 reducer 不能修改 state 状态,如果要返回一个新的状态,就必须返回一个新的对象。如此一来,Redux Store 状态树上某个节点如果没有改变,那么我们就有信心这节点下数据没有变化,应用在 reselect 中,步骤一的运算就可以确定直接缓存运算结果。

33. 虽然 reselect 的 createSelector 创造的选择器并不是一个纯函数,但是 createSelector 接受的所有函数参数都是纯函数,虽然选择器有“记忆”这个副作用,但是只要输入参数 state 没有变化,产生的结果也就没有变化,表现得却类似于纯函数。

34. 只要 Redux Store 状态树上的 filter 和 todos 字段不变,无论怎样触发 TodoList 的渲染过程,都不会引发没有必要的便利 todos 字段的运算,性能自然更快。

35. 虽然 reselect 库以 re 开头,但是逻辑上和 React/Redux 没有直接关系。实际上,在任何需要这种具有记忆的计算场合都可以使用 reselect,不过,对于 React 和 Redux 组合的应用,reselect 无疑能够提供绝佳的支持。

36. 对比反范式方式和范式方式的优劣,不难看出范式方式更合理。因为虽然 join 数据需要花费计算时间,但是应用了 reselect 之后,大部分情况下都会命中缓存,实际上也就没有花费很多计算时间了。

37. 我们了解了利用 react-redux 提供的 shouldComponenetUpdate 实现来提高组件渲染功能的方法,一个要诀就是要避免传递给其他组件的 prop 值是一个不同的对象,不然会造成无谓的重复渲染。

观后感回放:
粉丝路人甲:“我感觉自己开始吃成长快乐了耶”
锵哥:“从陌生到熟练,唯有坚持”
粉丝路人甲:“嗯,好”
锵哥:“加油!”
粉丝路人甲:“????????????”

广告:

本人从事全栈工程师,目前主要工作能力涵盖的范围有:android,ios,h5,pcWeb,react,vue,node,java 服务端,微信服务号,微信小程序,支付宝生活号,支付宝小程序。

本公众号会不定期的将自己的研发感悟,以及心得笔记无私奉献给大家。还等啥,赶快上车吧,铁子们!!!????(还会有其他的福利哦!快来吧)

官方订阅号:锵哥的觉悟
微信号:DY_suixincq
二维码:

退出移动版