Skip to content

merrier/mobile-scroll-events

Repository files navigation

移动端滚动事件大起底

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

移动端滚动事件介绍

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

滚动事件应用

我做了几个简单的demo,可以直接点击下面的链接或者扫描二维码查看(在手机上效果更佳~):

下拉刷新 吸顶效果 图片懒加载 侧边浮动导航栏
在线实例 在线实例 在线实例 在线实例
下拉刷新 吸顶效果 图片懒加载 侧边浮动导航栏

滚动事件分类和兼容性

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

全局滚动

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

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发现触发的时机;调用方式和全局滚动类似:

(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出现,就出现了针对于移动端滚动事件的各种兼容方案,总结如下:

  • 使用 ontouchmove 去替代 onscroll,虽然能更频繁的触发事件,但是这边的项目需求是实时响应滚动事件的同时,还要对页面元素进行重定位的DOM操作,由上述原因可知,在滚动过程中,页面会停止一切关于DOM方面的操作,所以若使用 ontouchmove 去实现的话,在按住屏幕进行滑动的时候,屏幕会出现元素抖动的情况(事件触发与DOM操作间具有几十毫秒的时间差),兼容失败
  • 使用 iscroll 的probe版本,该版本能实时探查到滚动的距离,但该钩子函数是实时去关注 requestAnimationFrame 下的状态,所以对浏览器的版本性能消耗很大,安卓机根本动不了,兼容失败
  • 使用 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手机上做一下测试……(如果你感兴趣的话,可以点击下面链接或扫描二维码进行测试,欢迎告知结果~)

全局滚动 局部滚动
测试demo 测试demo
全局滚动 局部滚动

滚动事件性能优化

除了兼容性问题以外,由于滚动事件和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 事件中剥离

参考文章

About

📜 移动端滚动事件大起底!

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published