最近在做移动端的项目,其中有个需求就是滚动监听标签页,实现用户滚动到不同位置时点亮相应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。
上面介绍的抖动与节流实现的方式都是借助了定时器 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 事件中剥离