logo头像

叩首问路,码梦为生

移动端滚动事件大起底

最近在做移动端的项目,其中有个需求就是滚动监听标签页,实现用户滚动到不同位置时点亮相应 tab 按钮;这其实是个很简单的需求,但是根据之前的项目经验,移动端的滚动事件会有各种坑,所以就花时间做了一些功课,对移动端滚动事件中的坑进行了总结,同时提供了一些解决方案。

移动端滚动事件介绍

我们这里要讲的是 onscroll 事件,具体参见MDN文档

滚动事件应用

我做了几个简单的 demo,可以直接点击下面的链接查看(在手机上效果更佳~),关于移动端滚动事件的简单 demo 我上传到了我的 github,可以点击这里查看源码:

滚动事件分类和兼容性

按照实际滚动的 dom 类型分为全局滚动和局部滚动

全局滚动

滚动条在 body 节点或者更顶层,一般是这样调用:

1
2
3
4
5
6
7
8
9
window.onscroll = function() {
var bHeight = document.body.clientHeight, // body对象高度,如果有滚动高度也包括
wHeight = window.innerHeight, // 浏览器窗口的视口
sTop = document.body.scrollTop; // body距离滚动顶部的距离
var isScrollBottom = bHeight - (wHeight + sTop) === 0 ? true : false;
if (isScrollBottom) {
// 执行相关代码
}
}

也可以通过 addEventListener 的方式

局部滚动

滚动条在 body 下某一个 dom 节点,在移动端如果使用局部滚动,意思就是我们的滚动在一个固定宽高的 div 内触发,将该 div 设置成 overflow:scroll/auto; 来形成 div 内部的滚动,这时我们监听 div 的 onscroll 发现触发的时机;调用方式和全局滚动类似:

1
document.getElementById('div1').onscroll = function(){})

兼容性

整体来看,全局滚动的兼容性要不如局部滚动,安卓比 IOS 兼容性更好:

body 滚动局部滚动
iOS不能实时触发不能实时触发
android实时触发实时触发
iOS WKWebview 内核实时触发实时触发

为什么 IOS 下的滚动事件会有各种 bug 呢,通过查阅资料,得出如下结论: iOS 的 webview 内核设定了其在进行 momentum scrolling (弹性滚动, 设置 -webkit-overflow-scrolling:touch 可以达到弹性滚动效果,安卓无效)时,会停止所有的 事件响应DOM操作引起的页面渲染,故 onscroll 不能实时响应,具体可以点击这里查看某位大牛写的实例 这里说明一下关于 iOS 的 wkwebview 内核是 iOS 从 iOS8 开始提供的新型 webview 内核,和之前的 uiwebview 相比,性能要好,具体大家可以自行查看关于 wkwebview 的相关概念

解决方案

既然 onscroll 事件在 iOS 和安卓上的表现并不统一,同时根据浏览器内核的不同会有潜在的 bug 出现,就出现了针对于移动端滚动事件的各种兼容方案,总结如下:

  1. 使用 ontouchmove 去替代 onscroll,虽然能更频繁的触发事件,但是这边的项目需求是实时响应滚动事件的同时,还要对页面元素进行重定位的 DOM 操作,由上述原因可知,在滚动过程中,页面会停止一切关于 DOM 方面的操作,所以若使用 ontouchmove 去实现的话,在按住屏幕进行滑动的时候,屏幕会出现元素抖动的情况(事件触发与 DOM 操作间具有几十毫秒的时间差),兼容失败
  2. 使用 iscroll 的 probe 版本,该版本能实时探查到滚动的距离,但该钩子函数是实时去关注 requestAnimationFrame 下的状态,所以对浏览器的版本性能消耗很大,安卓机根本动不了,兼容失败
  3. 使用 swiper 插件,在启动 freeMode 模式时模拟原生的弹性滚动( swiper 模拟原生滚动的方案能兼容较多的安卓机型不出现bug,推荐), 因为 swiper 没有实时监听滚动位置的功能,故我监听滚动开始及结束后的事件,通过 setInterval 及一些计算去实现滚动条的监听,但因为 react 元素的变化量比较大,导致 swiper 在移动端时对父容器的计算速率达到了一个瓶颈,依旧出现很卡顿的现象,兼容失败

通过以上的兼容性尝试,可以发现其实并没有一个完美的解决方案,所以如果真的需要达到某些移动端滚动效果的话,可以采取 fallback 方案:

  • Android 用 scroll 方案,因为兼容性很可观;
  • IOS 如果不需要兼容 8 以下版本的话,就也直接用 scroll 方案,因为 wkwebview 已经对滚动事件进行了优化,而如果需要兼容8以下版本的话,可以考虑 isroll 或 JRroll 这两种插件,同时需要真机测试查看效果是否达到要求(尤其是 QQ 浏览器和搜狗浏览器)

由于查询到的资料比较老旧,对于滚动事件的兼容性描述可能已经过时了,我就在最近(2017-8-24)用各种浏览器测试了一下iOS中的滚动事件的兼容性,总结如下:

是否可以弹性滚动是否需要设置 overflow:scrolling 才能弹性滚动设置 overflow:scrolling 之后,滚动期间是否监听事件未设置 overflow:scrolling,滚动期间是否监听事件
safari(v10.0)-
chrome(v60.0)
firefox(v8.2)-
weixin(v6.5.14)
QQ(v7.7.2)
搜狗(v5.8.1)

从上面的表格可以看到,最新版的 iOS 浏览器其实并不需要 overflow:scrolling 就可以实现弹性滚动,同时除了搜狗浏览器之外,其他浏览器在滚动期间都会监听事件,由此可见截止到目前(2017-8-24),iOS 和浏览器对滚动事件的兼容性已经做了很多优化和改进了,之后有时间的话再在 android 手机上做一下测试……

滚动事件性能优化

除了兼容性问题以外,由于滚动事件和 resize 事件同属于会频繁触发的事件。如果事件中涉及到大量的位置计算、DOM 操作、元素重绘等工作且这些工作无法在下一个 scroll 事件触发前完成,就会造成浏览器掉帧。

防抖和节流

scroll 事件本身会触发页面的重新渲染,同时 scroll 事件的 handler 又会被高频度的触发, 因此事件的 handler 内部不应该有复杂操作,例如 DOM 操作就不应该放在事件处理中。 针对此类高频度触发事件问题(例如页面 scroll ,屏幕 resize,监听用户输入等),下面介绍两种常用的解决方法,防抖和节流(underscore和lodash里面有封装好的这两种方法,感兴趣的话可以研究一下源码)。

防抖

防抖技术即是可以把多个顺序地调用合并成一次,也就是在一定时间内,规定事件被触发的次数。

节流

防抖函数确实不错,但是也存在问题,譬如图片的懒加载,我希望在下滑过程中图片不断的被加载出来,而不是只有当我停止下滑时候,图片才被加载出来。又或者下滑时候的数据的 ajax 请求加载也是同理。 这个时候,我们希望即使页面在不断被滚动,但是滚动 handler 也可以以一定的频率被触发(譬如 250ms 触发一次),这类场景,就要用到另一种技巧,称为节流函数(throttling)。 节流函数,只允许一个函数在 X 毫秒内执行一次。 与防抖相比,节流函数最主要的不同在于它保证在 X 毫秒内至少执行一次我们希望触发的事件 handler。

使用 rAF(requestAnimationFrame)触发滚动事件

上面介绍的抖动与节流实现的方式都是借助了定时器 setTimeout ,但是如果页面只需要兼容高版本浏览器或应用在移动端,又或者页面需要追求高精度的效果,那么可以使用浏览器的原生方法 rAF(requestAnimationFrame)。 window.requestAnimationFrame() 这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数。这个方法接受一个函数为参,该函数会在重绘前调用。 rAF 常用于 web 动画的制作,用于准确控制页面的帧刷新渲染,让动画效果更加流畅,当然它的作用不仅仅局限于动画制作,我们可以利用它的特性将它视为一个定时器。(当然它不是定时器) 通常来说,rAF 被调用的频率是每秒 60 次,也就是 1000 / 60 ,触发频率大概是 16.7ms 。(当执行复杂操作时,当它发现无法维持 60fps 的频率时,它会把频率降低到 30fps 来保持帧数的稳定。)

总结一下

  • 防抖动:防抖技术即是可以把多个顺序地调用合并成一次,也就是在一定时间内,规定事件被触发的次数。
  • 节流函数:只允许一个函数在 X 毫秒内执行一次,只有当上一次函数执行后过了你规定的时间间隔,才能进行下一次该函数的调用。
  • rAF:16.7ms 触发一次 handler,降低了可控性,但是提升了性能和精确度。

从本质上而言,我们应该尽量去精简 scroll 事件的 handler ,将一些变量的初始化、不依赖于滚动位置变化的计算等都应当在 scroll 事件外提前就绪。建议:避免在 scroll 事件中修改样式属性 / 将样式操作从 scroll 事件中剥离

参考文章

支付宝打赏 微信打赏

听说赞过就能年薪百万