列表项上下键选中使用 scrollIntoView behavior: ‘smooth’ 抖动问题
原理分析
放弃计算DOM getBoundingClientRect的方式,也不使用原版每次选中都进行 scrollIntoView 自适应 ‘smooth’ 滚动,而是采用底层 API Intersection Observer API 来计算列表项(list-item)是否存在于可视的列表范围(list-container)之内,底层 API 性能、效率、效果都比前面两种方式高的多。
Intersection Observer 从用法上还是蛮简单的,那理解上来说呢,简单来讲:是一个观察者模式,目标元素与视口会产生一个是否可见交叉区,通过观察视口与目标元素的交叉关系,可以在 callback 回调中获取目标元素与视口的交叉关系信息,产生”交叉观察器”的效果。
特性: callback 一般只会触发两次,一次是目标元素开始可见,另一次是开始不可见。
可行性分析
那在个人的项目例子中呢,所有的 list-item 都是目标元素,通过观察完全超出视口范围的第一个 list-item 开始可见的时间点来引起 scrollIntoView 平滑动画过渡的 ‘smooth’ 滚动,在滚动之后视口有新的一批 list-item 的情况下,重建 IntersectionObserver,重置 IntersectionObserver 实例,以此来循环。
可以这么想,”交叉观察器”观察的永远是新的一批 list-item 与视口的交叉关系,而老一批 list-item 在 scrollIntoView 滚动之后,因”交叉观察器”特性(只会触发两次)应当重建”交叉观察器”重新观察。
在这时候可能想问,有没有可能存在一种情况,scrollIntoView 平滑动画过渡的 ‘smooth’ 滚动的时间会慢于按键的速度,通过完全超出视口范围的第二个、第三个等 list-item 开始可见的时间点再次引起滚动,从而产生抖动的问题呢?答案是理论上存在这种可能性,而在实际操作上,极端操作极速按键按住不放的情况下存在,而在比较快按键的节奏下,是不存在这种可能性的。
- 首先,在第一次滚动之后,第一时间就对 IntersectionObserver 实行重建,IntersectionObserver 实例实行重置,将源实例置为null并停止对所有目标元素可见性变化的观察,虽然观察者本身仍然处于活动状态,在 disconnect() 之后,目标元素仍然可以通过 observe() 传递给观察者,但没有目标,在实例置为null后,会很快被垃圾回收机制处理,所以这时候是不会存在的。
- 其次,即使按键按住不放极速实行选中,也可以通过节流来解决,因为按键按住不放这种操作,本身就是一种非常规且触及极端边界的操作,不必实时且完全满足这种极端操作。
PS: 这种重建 IntersectionObserver,重置 IntersectionObserver 实例的方式确实有一些不是很优雅,但官方并没有提供动态修改的方法,不可能把希望寄托在官方修改api上,了解ECMA的提案流程的同学肯定知道,那将是个十分漫长的过程,所以眼下就只能找到这种比较切实的办法来解决这个问题。
Attention: 原生事件的监听会阻断 scrollIntoView ‘smooth’ 滚动动画的执行,产生滑动卡顿,所以必须取消默认事件的执行( e.preventDefault() )或者直接取消监听事件。
实际演示