场景
页面在滚动过程中,根据当前视图中展示的模块聚焦到对应的标签上。
版本1(可直接查看优化的版本2)
第一个版本是在微信小程序上实现的,当时的想法就非常的简单粗暴,也正因为如此,该页面的性能非常的差。基本的实现方式如下:
- 获取每个模块在当前页面的尺寸
- 监听页面的
onPageScroll
事件,在回调事件中遍历每个模块的的尺寸和位置配合scrollTop
进行计算,得到一个合适的聚焦索引 - 针对模块的高度是否大于可视区实现不同的计算,有大量的分支逻辑
查看了微信官方的渲染性能优化,不禁打了个寒颤,这简直就是在性能雷区上蹦迪😱~
版本2
这个页面本质还是一个长列表,于是就靠着虚拟列表的方向探索,但是由于该页面交互还存在着点击某个标签定位到具体模块的交互,虚拟列表在快速滚动的时候存在白屏现象,一开始的点击跳转也会因为位置不准确导致定位不准。虚拟列表实现上使用的Intersection Observer API
吸引了我的目光。而微信API中也有对应的实现,果断开始尝试。
Intersection Observer API 存在的问题
- 初始化
Observer
的时候需要设置相交阈值,该相交阈值指的是相交区域 / 目标元素 ,而我期望该值对应的是相交区域 / 参照元素。若设置了一个相对比较大的阈值,若目标元素的尺寸是参照元素的几十倍的时候,得到的值会无限接近于0,也就无法触发相应的回调 - 列表中单个模块的高度参差不齐,一个可视区中可以存在多个模块,也可能无法容纳一个模块
如上图所示,模块0, 1, 2 都会触发相交回调,但是聚焦索引不是2,而是0。
解决
针对第一个问题,若把阈值列表中的值分散的太细,则触发的次数过多,没有必要。只监听模块进入视图区域和离开视图区域的情况。默认的Observer配置就可以达到这个效果。
针对问题二,若遇到上述情形。0,1,2三个模块都触发了,但是索引只聚焦在0处。至于什么时候聚焦到1处,则是0退出区域的时候。而队列的数据结构正好可以模拟这种状态。第一个元素离开的时候,从队列中把队头的数据取出。每次取队头的元素作为当前的聚焦索引。
if (isIntersecting) {
queue.push(id); // 模块进入
} else {
queue.shift(); // 模块离开
}
特殊情况处理
若列表仅朝着单一的方向且不存在来回滑动的时候,以上的队列方式可以正常操作。若页面存在来回滚动的情况,需要增加一步特殊处理。
上图中,页面滚到底的时候,此时队列中有3, 4。
此时若向上滚动,4(最后一个模块)离开区域就需要把队尾元素弹出,显然此时队尾元素是3,清除后此时的模块依旧聚焦在4。针对这种情况,将队列中的数据逆置,再进行队尾元素的弹出操作。同理,向上划到顶部之后再向下滚动也是这种情况。
if (entry.isIntersecting) {
queue.push(id);
} else {
if (
queue.length > 0 &&
(activeKey == 0 || activeKey == lastIndex) &&
(queue[queue.length - 1] == activeKey)
) {
queue.reverse();
}
queue.shift();
}
若没有滑动到底部或者顶部的时候改变滚动属性,此时也要相应的调整队列中的顺序,始终保持队列中数据递增或者递减。
if (entry.isIntersecting) {
if (queue.length > 0 && Math.abs(id - queue[queue.length - 1]) !== 1) {
queue.reverse();
}
queue.push(id);
} else {
if (
queue.length > 0 &&
(activeKey == 0 || activeKey == lastIndex) &&
queue[queue.length - 1] == activeKey
) {
queue.reverse();
}
queue.shift();
}
插入过程中限制来相邻两个数的差值的绝对值为1并不能保证队列中没有数组重复,引入Set数据结构保证数据的唯一性,可以改写为:
if (isIntersecting) {
if (!queueSet.has(id)) {
if (queue.length > 0 && Math.abs(id - queue[queue.length - 1]) !== 1) {
queue.reverse();
}
queue.push(id);
queueSet.add(id);
}
} else {
if (
queue.length > 0 &&
(activeKey.value == 0 || activeKey.value == lastIndex) &&
queue[queue.length - 1] == activeKey.value
) {
queue.reverse();
}
if (id == queue[0]) {
queueSet.delete(queue[0]);
queue.shift();
}
}
队列中的数据维护需要保证以下几点:
- 数据唯一
- 递增/递减(始终保持)
交互优化
if (queue.length > 0) {
if (queue[queue.length - 1] == 0 || queue[queue.length - 1] == lastIndex) {
activeKey.value = queue[queue.length - 1];
} else {
activeKey.value = queue[0];
}
}