Intersection Observer API实现模块滚动聚焦

504 阅读4分钟

场景

页面在滚动过程中,根据当前视图中展示的模块聚焦到对应的标签上。 demo

版本1(可直接查看优化的版本2)

第一个版本是在微信小程序上实现的,当时的想法就非常的简单粗暴,也正因为如此,该页面的性能非常的差。基本的实现方式如下:

  • 获取每个模块在当前页面的尺寸
  • 监听页面的onPageScroll事件,在回调事件中遍历每个模块的的尺寸和位置配合scrollTop进行计算,得到一个合适的聚焦索引
  • 针对模块的高度是否大于可视区实现不同的计算,有大量的分支逻辑 查看了微信官方的渲染性能优化,不禁打了个寒颤,这简直就是在性能雷区上蹦迪😱~ 301647781156_ pic

版本2

这个页面本质还是一个长列表,于是就靠着虚拟列表的方向探索,但是由于该页面交互还存在着点击某个标签定位到具体模块的交互,虚拟列表在快速滚动的时候存在白屏现象,一开始的点击跳转也会因为位置不准确导致定位不准。虚拟列表实现上使用的Intersection Observer API吸引了我的目光。而微信API中也有对应的实现,果断开始尝试。

Intersection Observer API 存在的问题

  • 初始化Observer的时候需要设置相交阈值,该相交阈值指的是相交区域 / 目标元素 ,而我期望该值对应的是相交区域 / 参照元素。若设置了一个相对比较大的阈值,若目标元素的尺寸是参照元素的几十倍的时候,得到的值会无限接近于0,也就无法触发相应的回调
  • 列表中单个模块的高度参差不齐,一个可视区中可以存在多个模块,也可能无法容纳一个模块 截屏2022-03-20 下午9 13 10

如上图所示,模块0, 1, 2 都会触发相交回调,但是聚焦索引不是2,而是0。

解决

针对第一个问题,若把阈值列表中的值分散的太细,则触发的次数过多,没有必要。只监听模块进入视图区域和离开视图区域的情况。默认的Observer配置就可以达到这个效果。

针对问题二,若遇到上述情形。0,1,2三个模块都触发了,但是索引只聚焦在0处。至于什么时候聚焦到1处,则是0退出区域的时候。而队列的数据结构正好可以模拟这种状态。第一个元素离开的时候,从队列中把队头的数据取出。每次取队头的元素作为当前的聚焦索引。 Untitled

if (isIntersecting) { 
  queue.push(id); // 模块进入
} else {
  queue.shift(); // 模块离开
}

特殊情况处理

若列表仅朝着单一的方向且不存在来回滑动的时候,以上的队列方式可以正常操作。若页面存在来回滚动的情况,需要增加一步特殊处理。 截屏2022-03-20 下午9 30 17

上图中,页面滚到底的时候,此时队列中有3, 4。 截屏2022-03-20 下午9 31 19 此时若向上滚动,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();
  }
}

队列中的数据维护需要保证以下几点:

  • 数据唯一
  • 递增/递减(始终保持)

交互优化

截屏2022-03-20 下午11 03 02 如上图所示,划到底部的时候,队列中的第一个元素应该是3,若按照此种方式,就永远没有机会聚焦到模块4上,针对最后一个模块,若队列中的最后一个元素是最后一个模块,则直接用该模块作为聚焦模块。底部向上回归到顶部的时候同理;
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];
  }
}

Demo代码

github.com/SunYiwen/bl…