08-面试讲解
# 虚拟列表面试讲解
# 技术点图谱
涉及到的技术点如下:
动态高度
白屏闪烁
长列表其他解决方案
- 时间分片
其他优化点
# 难点描述
模拟问题:我看到你项目亮点里面写了一条“优化虚拟列表的渲染”,请你说一下你具体是如何进行优化的么?
问题分析
- 当时遇到的问题?
- 你拿到这个问题后,你的一个思考过程
- 你思考后落地的方案
- 落地方案的一个效果
参考答案
当时我们最早那一版虚拟列表组件,因为初始需求比较固定,因此采用的是定高的方式来写的,虽然写的时候很方便,不过灵活性上面比较差,将就能用。后面由于业务需求有变化,列表项目里面会包含一些可变内容,所以之前那种定高的方案就不再可行了,这是第一个需要优化的点,支持动态高度。另外还有就是第一版虚拟列表没有设置缓冲区,在用户滚动过快的情况下,偶尔也会出现白屏闪烁的现象,这也是需要解决的一个问题。(交代问题背景)
所以我主要就是对这两个问题着手进行优化。
针对第一个问题,主要需要考虑的地方有:
- 该如何获取真实的高度?
- 和列表项相关的计算有些什么变化?
- 列表渲染的方式又有什么样的变化?
而第二个问题比较简单,在可视区的基础上加上缓冲区就行了。不过添加了缓冲区后,部分计算也需要更新。添加了缓冲区后,白屏闪烁的问题就完全被解决了。(你的一个思考过程,以及你思考出来的解决方案)
当然这里面涉及到的细节还是很多的,面试老师您看需不需要我把这些细节展开讲一下。(钩子🪝)
# 技术点描述
# 1. 动态高度
模拟问题:那你先说一下动态高度里面有哪些细节要处理?
问题分析
- 首先回答需求变为动态高度,计算方面有哪些变化
- 针对这些变化你是如何思考以及如何处理的,这里在回答的时候稍微说一点细枝末节的东西
- 设置下一个钩子,引到白屏闪烁的话题
参考答案
首先列表项变为动态高度后,会存在这么一些问题:
- 如何获取列表项的真实高度?
- 和列表项高度相关的那些计算该如何变化?
- 列表的渲染是否会发生变化
我刚开始考虑的第一种方案,是将列表项的高度值扩展成一个数组,数组里面包含所有的可能性,例如
[50 ,20 ,100, ...]
但是这种方案仍然不太好,因为说不好某个列表项的高度就不在我所设定的范围里面。
后来我又考虑能否将列表项先渲染到屏幕外,对其高度进行测量并缓存,然后再将其渲染至可视区域内。但是这样也不太好,预先渲染至屏幕外,再渲染至屏幕内,这会导致渲染成本增加一倍。(体现了你拿到这个问题后,你的一个思考过程)
最终我选择了采用预估高度先行渲染的方案,先创建一个高度缓存列表,里面存储每个列表项预估的高度值,并按照这个预估值来渲染。之后有了真实高度后,在 updated 生命周期钩子方法里面更新缓存列表里面的高度值。(你最终选择的落地方案)
不过这里涉及到一个细节,就是因为高度不定,所以获取开始索引值的方式也会有所改变。
之前定高,要获取起始索引值,直接 scrollTop 除去列表项高度就行了,现在不定高的话,起始索引的计算应该是在缓存列表中搜索第一个底部定位大于列表垂直偏移量的列表项,然后返回其索引。关于如何搜索第一个符合要求列表项,这里也有一个优化,我一开始使用的是 find 方法,但是后来我琢磨着这个缓存列表是一个有序的数组,那么使用二分查找效率会更高一些,时间复杂度相比之前的 O(n) 优化为了 O(logN)(说一些细节体现你对这个东西很熟,自然而然引出二分查找的优化,体现你的做事儿风格)
动态高度处理这一块儿的细节差不多就这么多,相比白屏闪烁那一块儿的细节确实要多一些。(钩子🪝)
# 2. 白屏闪烁
模拟问题:那么白屏闪烁这一块儿,有一些什么细节需要处理呢?
问题分析
- 你的思考过程
- 你的落地方案
- 落地方案的效果
参考答案
首先我想了想为什么会出现白屏闪烁,发现主要是因为用户滚动过快,没有给新列表项的渲染留足时间,于是我在原来列表结构的基础上,添加了缓冲区,这样整个渲染区域就由可视区+缓冲区组成。(阐述问题的原因以及你思考的一个方案)
当然,既然渲染区域的结构发生了改变,那么很多设计上面也会有相应的变动。例如我增加了一个比例值的props,方便用户调整缓冲区列表项的个数,还有就是整个列表项个数的计算,也需要将缓冲区的内容计算进去,这就涉及到两个起始和结束索引的修改。(稍微阐述一些细节,体现你对这个很熟悉)
有了缓冲区后,白屏闪烁的问题就彻底解决了,因为缓冲区的存在,有了充足的时间渲染新列表项。(最终落地的效果)
# 3. 长列表常见解决方案
模拟问题:那么除了虚拟列表以外,长列表还有其他解决方案么?
问题分析
- 首先阐述长列表的核心痛点?
- 再阐述能够从哪些方面来解决这个问题?
- 然后再对比一下各种方案的优缺点,突出虚拟列表是最好的(可以从虚拟列表的原理入手)
参考答案
有倒是有,其实所有的解决方案都是为了解决长列表的核心痛点,那就是列表项过多导致的渲染耗时,性能低下,页面卡顿。(阐述长列表一个本质上的问题)
早期有一种基于时间分片的方案,例如使用 requestAnimationFrame、requestIdleCallback 这些浏览器 API,由浏览器来决定回调函数的执行时机。大量的数据会被分多次渲染,每次渲染对应一个片段。在每个片段中处理定量的数据后,就会将主线程还给浏览器,从而解决页面卡顿的问题。但是时间切片仅仅也就只能解决卡顿这个问题,你分多个时间段来渲染,渲染依然是耗时的,并且最终仍然是有大量的列表项存在于页面上,性能依然低下。(阐述时间切片这个解决方案,以及该方案有哪些缺陷)
现在比较成熟且通用的方案,基本都是用虚拟列表。这种方案的原理是设置一个可视区域,然后用户在滚动列表的时候,本质上是动态修改可视区域里面的内容,所渲染的列表项数量始终是固定的,因此同时解决了渲染耗时,性能低下和页面卡顿的问题。(从虚拟列表实现原理上来解释为什么这种方案是最好的)
当然,其实我项目里面写的第二版虚拟列表,其实还有优化的空间,回头有时间我会再写一版,然后发布到 npm 上面。(钩子🪝)
# 4. 还有哪些优化点
模拟问题:你说还有优化的空间,具体是哪些地方呢?
问题分析
回答的时候大致说一下优化的方向即可。
参考答案
在我第二版的虚拟列表实现中,仍然用的是监听 scroll 事件的方式来触发可视区域数据的更新。但是有个问题,当滚动发生后,scroll 事件会频繁的触发,很多时候会造成重复计算的问题,这在性能上面其实是一种浪费。
这里其实可以使用 IntersectionObserver 来替换监听 scroll 事件,相比 scroll,IntersectionObserver 可以设置多个阈值来检测元素进入视口的不同程度,只在必要时才进行计算,没有性能上的浪费。并且监听回调也是异步触发的。
另外还有就是不定高这一块儿,如果列表项中包含图片,并且列表高度由图片撑开,由于图片会发送网络请求,此时无法保证在获取列表项真实高度时图片是否已经加载完成,从而造成计算不准确的情况。这种情况下,需要监听列表项的大小变化从而获取真正的高度。这里可以使用 ResizeObserver 来做这一层的监听,当尺寸发生变化后,ResizeObserver 会监听到,然后可以获取每一列表项的高度。(阐述还能够做哪些优化)
当时之所以这一块儿没有优化,是因为这两个 API 的浏览器覆盖率较低,只有极少浏览器支持这两个 API。不过现在这两个 API 的支持度已经比较好了,可以考虑替换以前的一些做法了,当然,我也会考虑回退机制,从而支持低版本的浏览器。(解释为什么之前没有用这个优化方案,顺便说要考虑回退机制,体现你的做事儿风格)
不知道这一块儿我还有没有没考虑完善的地方,面试老师您认为还有其他需要优化的地方么?
(结束🎉)
-EOF-