前端面试题--React篇
说说React diff的原理是什么?
是什么
跟Vue
一致,React
通过引入Virtual DOM
的概念,极大地避免无效的Dom
操作,使我们的页面的构建效率提到了极大的提升
而diff
算法就是更高效地通过对比新旧Virtual DOM
来找出真正的Dom
变化之处
传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),react
将算法进行一个优化,复杂度姜维O(n)
O(n^3) :将两颗树中所有的节点一一对比需要O(n²)的复杂度,在对比过程中发现旧节点在新的树中未找到,那么就需要把旧节点删除,删除一棵树的一个节点(找到一个合适的节点放到被删除的位置)的时间复杂度为O(n),同理添加新节点的复杂度也是O(n),合起来diff两个树的复杂度就是O(n³)
原理
react
中diff
算法主要遵循三个层级的策略:
- tree层级
- conponent 层级
- element 层级
tree层级
DOM
节点跨层级的操作不做优化,只会对相同层级的节点进行比较,只有删除、创建操作,没有移动操作。(例如react
发现新树中,R节点下没有了A,那么直接删除A,在D节点下创建A以及下属节点。上述操作中,只有删除和创建操作)
conponent层级
如果是同一个类的组件,则会继续往下diff
运算,如果不是一个类的组件,那么直接删除这个组件下的所有子节点,创建新的
当component D
换成了component G
后,即使两者的结构非常类似,也会将D
删除再重新创建G
element层级
对于比较同一层级的节点们,每个节点在对应的层级用唯一的key
作为标识
提供了 3 种节点操作,分别为 INSERT_MARKUP
(插入)、MOVE_EXISTING
(移动)和 REMOVE_NODE
(删除)
如下场景:
通过key
可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置
- index: 新集合的遍历下标。
- oldIndex:当前节点在老集合中的下标
- maxIndex:在新集合访问过的节点中,其在老集合的最大下标
如果当前节点在新集合中的位置比老集合中的位置靠前的话,是不会影响后续节点操作的,这里这时候被动字节不用动
操作过程中只比较oldIndex和maxIndex,规则如下:
- 当oldIndex>maxIndex时,将oldIndex的值赋值给maxIndex
- 当oldIndex=maxIndex时,不操作
- 当oldIndex<maxIndex时,将当前节点移动到index的位置
diff
过程如下:
- 节点B:此时 maxIndex=0,oldIndex=1;满足 maxIndex< oldIndex,因此B节点不动,此时maxIndex= Math.max(oldIndex, maxIndex),就是1
- 节点A:此时maxIndex=1,oldIndex=0;不满足maxIndex< oldIndex,因此A节点进行移动操作,此时maxIndex= Math.max(oldIndex, maxIndex),还是1
- 节点D:此时maxIndex=1, oldIndex=3;满足maxIndex< oldIndex,因此D节点不动,此时maxIndex= Math.max(oldIndex, maxIndex),就是3
- 节点C:此时maxIndex=3,oldIndex=2;不满足maxIndex< oldIndex,因此C节点进行移动操作,当前已经比较完了
当ABCD节点比较完成后,diff
过程还没完,还会整体遍历老集合中节点,看有没有没用到的节点,有的话,就删除
注意事项
对于简单列表渲染而言,不使用key
比使用key
的性能,例如:
将一个[1,2,3,4,5],渲染成如下的样子:
1 |
|
后续更改成[1,3,2,5,4],使用key
与不使用key
作用如下:
1 |
|
如果我们对这个集合进行增删的操作改成[1,3,2,5,6]
1 |
|
由于dom
节点的移动操作开销是比较昂贵的,没有key
的情况下要比有key
的性能更好
说说 React 性能优化的手段有哪些
是什么
React
凭借virtual DOM
和diff
算法拥有高效的性能,但是某些情况下,性能明显可以进一步提高
在前面我们了解到类组件通过调用setState
方法, 就会导致render
,父组件一旦发生render
渲染,子组件一定也会执行render
渲染
当我们想要更新一个子组件的时候,如下图绿色部分:
理想状态只调用该路径下的组件render
:
但是react
的默认做法是调用所有组件的render
,再对生成的虚拟DOM
进行对比(黄色部分),如不变则不进行更新
从上图可见,黄色部分diff
算法对比是明显的性能浪费的情况
如何做
在React中如何避免不必要的render (opens new window)中,我们了解到如何避免不必要的render
来应付上面的问题,主要手段是通过shouldComponentUpdate
、PureComponent
、React.memo
,这三种形式这里就不再复述
除此之外, 常见性能优化常见的手段有如下:
- 避免使用内联函数
- 使用 React Fragments 避免额外标记
- 使用 Immutable
- 懒加载组件
- 事件绑定方式
- 服务端渲染
[#](https://vue3js.cn/interview/React/Improve performance.html#避免使用内联函数)避免使用内联函数
如果我们使用内联函数,则每次调用render
函数时都会创建一个新的函数实例,如下:
1 |
|
我们应该在组件内部创建一个函数,并将事件绑定到该函数本身。这样每次调用 render
时就不会创建单独的函数实例,如下:
1 |
|
使用 React Fragments 避免额外标记
用户创建新组件时,每个组件应具有单个父标签。父级不能有两个标签,所以顶部要有一个公共标签,所以我们经常在组件顶部添加额外标签div
这个额外标签除了充当父标签之外,并没有其他作用,这时候则可以使用fragement
其不会向组件引入任何额外标记,但它可以作为父级标签的作用,如下所示:
1 |
|
事件绑定方式
在事件绑定方式 (opens new window)中,我们了解到四种事假绑定的方式
从性能方面考虑,在render
方法中使用bind
和render
方法中使用箭头函数这两种形式在每次组件render
的时候都会生成新的方法实例,性能欠缺
而constructor
中bind
事件与定义阶段使用箭头函数绑定这两种形式只会生成一个方法实例,性能方面会有所改善
使用 Immutable
在理解Immutable中 (opens new window),我们了解到使用 Immutable
可以给 React
应用带来性能的优化,主要体现在减少渲染的次数
在做react
性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()
中做对比,当返回true
执行render
方法
Immutable
通过is
方法则可以完成对比,而无需像一样通过深度比较的方式比较
https://zhuanlan.zhihu.com/p/20295971
懒加载组件
从工程方面考虑,webpack
存在代码拆分能力,可以为应用创建多个包,并在运行时动态加载,减少初始包的大小
而在react
中使用到了Suspense
和 lazy
组件实现代码拆分功能,基本使用如下:
1 |
|
服务端渲染
采用服务端渲染端方式,可以使用户更快的看到渲染完成的页面
服务端渲染,需要起一个node
服务,可以使用express
、koa
等,调用react
的renderToString
方法,将根组件渲染成字符串,再输出到响应中
例如:
1 |
|
客户端使用render方法来生成HTML
1 |
|
其他
https://vue3js.cn/interview/React/Improve%20performance.html#%E5%85%B6%E4%BB%96
除此之外,还存在的优化手段有组件拆分、合理使用hooks
等性能优化手段…
总结
通过上面初步学习,我们了解到react
常见的性能优化可以分成三个层面:
- 代码层面
- 工程层面
- 框架机制层面
通过这三个层面的优化结合,能够使基于react
项目的性能更上一层楼