滚动

JS 相关

判断元素在视口内

当你需要实现图片懒加载或者无限滚动时,需要确定元素是否出现在视窗中。这可以在事件监听器中处理,最常见的解决方案是使用element.getBoundingClientRect()

window.addEventListener('scroll', () => {
  const rect = elem.getBoundingClientRect();
  // rect.left/top/right/bottom 是相对于视口的左上角位置而言的
  // 判断整个元素是否完全在视口之内
  const inViewport = rect.bottom > 0 && rect.right > 0 &&
                     rect.left < window.innerWidth &&
                     rect.top < window.innerHeight;
});
1
2
3
4
5
6
7
8

上述代码的问题在于每次调用getBoundingClientRect时都会触发回流,严重地影响了性能。在事件处理函数中调用(getBoundingClientRect)尤为糟糕,就算使用了函数节流(的技巧)也可能对性能没多大帮助。 (回流是指浏览器为局部或整体地重绘某个元素,需要重新计算该元素在文档中的位置与形状。)

在2016年后,可以通过使用 Intersection Observer 这一 API 来解决问题。它允许你追踪目标元素与其祖先元素或视窗的交叉状态。此外,尽管只有一部分元素出现在视窗中,哪怕只有一像素,也可以选择触发回调函数:

const observer = new IntersectionObserver(callback, options);
observer.observe(element);
1
2

此 API 被广泛地支持,但仍有一些浏览器需要 polyfill。尽管如此,它仍是目前最好的解决方案。

文档滚动加载

window.addEventListener('scroll', () => {
  // 获取文档的垂直滚动距离
  const documentScrollY = window.pageYOffset;
  // 获取文档的高度
  const documentHeight = document.body.offsetHeight;
  // 获取视口高度
  const viewPortHeight = window.innerHeight;
  // 文档底部距离视口底部的距离
  const leftDistance = documentHeight - documentScrollY - viewPortHeight;
  if (leftDistance <= 20) {
    // 文档滑动时,文档底部距离视口底部还有 20 px 时,加载新数据
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

滚动穿透问题、滚动边界问题

如果你的弹框或下拉列表是可滚动的,那你务必要了解连锁滚动相关的问题:当用户滚动到(弹框或下拉列表)末尾(后再继续滚动时),整个页面都会开始滚动。

滚动穿透问题解决

passive: true

addEventListener(type, listener[, options ])options里的passive参数,设置为true时,表明注册的listener内不会调用preventDefault(),浏览器将同时执行listener和浏览器的默认行为(而不是等执行listener结束之后再执行默认行为),且会忽略listener里的preventDefault(),使得滚动更加流畅。

Reference:

CSS 相关

iOS 滚动回弹效果

iOS 上若不做处理,滚动将显得不流畅。此时,可添加一行代码:

.scroll-area {
    -webkit-overflow-scrolling: touch;
}
1
2
3

-webkit-overflow-scrolling属性控制元素在移动设备上是否使用滚动回弹效果。

取值如下:

  • auto
    • Use "regular" scrolling, where the content immediately ceases to scroll when you remove your finger from the touchscreen.
    • 翻译:使用普通滚动, 当手指从触摸屏上移开,滚动会立即停止。 |
  • touch
    • Use momentum-based scrolling, where the content continues to scroll for a while after finishing the scroll gesture and removing your finger from the touchscreen. The speed and duration of the continued scrolling is proportional to how vigorous the scroll gesture was. Also creates a new stacking context.
    • 翻译:使用具有回弹效果的滚动, 当手指从触摸屏上移开,内容会继续保持一段时间的滚动效果。继续滚动的速度和持续的时间和滚动手势的强烈程度成正比。同时也会创建一个新的堆栈上下文。 |

需要注意的是,对容器添加了-webkit-overflow-scrolling: touch后,可能会存在以下问题:

  • 导致容器内使用position:fixed;固定定位的元素随着页面一起滚动。
  • (iOS UIWebview 里)容器内溢出的内容(比如弹窗)将被隐藏,效果类似于overflow: hidden,google 未找到原因

注意

该特性是非标准的,详情请见MDN 之 -webkit-overflow-scrollingopen in new window

position: sticky

position: sticky是结合了position: relativeposition: fixed两种定位功能于一体的特殊定位,适用于一些特殊场景。

元素先按照普通文档流定位,然后相对于该元素在流中的 flow root(BFC)和 containing block(最近的块级祖先元素)定位。而后,元素定位表现为在跨越特定阈值前为相对定位,之后为固定定位。

这个特定阈值指的是toprightbottomleft之一,换言之,指定toprightbottomleft四个阈值其中之一,才可使粘性定位生效。否则其行为与相对定位相同。

滚动条隐藏但可滚动

/* Chrome,Safari 和 Opera */
.container::-webkit-scrollbar {
  display: none;
}

/* IE 或 Edge */
.container {
  -ms-overflow-style: none;
}
1
2
3
4
5
6
7
8
9

至于 Firefox,没有任何办法隐藏滚动条。

滚动条样式

IE 5.5 版本以后,允许修改滚动条的颜色。

body {
  scrollbar-face-color: blue;
}
1
2
3

WebKit 的开发者在 2009 年提出了(修改滚动条)样式的方案。以下是使用 -webkit 前缀在支持相关样式的浏览器中模拟 macOS 滚动条样式的代码:

::-webkit-scrollbar {
  width: 8px;
}
::-webkit-scrollbar-thumb {
  background-color: #c1c1c1;
  border-radius: 4px;
}
1
2
3
4
5
6
7

Chrome、Safari、Opera 甚至于 UC 浏览器或者三星自带的桌面浏览器都支持(上述 CSS)。Edge 也有计划实现它们。但三年过去了,该计划仍在中等优先级中(而尚未被实现)。

锚点切换时,流畅的滚动

通过锚点链接来跳转到页面上的不同区块时,若想实现平滑地滚动,可添加一行代码:

html {
  scroll-behavior: smooth;
}
1
2
3

目前scroll-behavior仅在 Chrome、 Firefox 与 Opera 上被支持,但我们希望它能被广泛支持,因为使用 CSS (比使用 JavaScript)在解决页面滚动问题时优雅得多,并更符合“渐进增强”的模式。

Reference: