移动端滚动研究

移动web滚动问题在移动端如果使用局部滚动,意思就是我们的滚动在一个固定宽高的div内触发,将该div设置成overflow:scroll/auto;来形成div内部的滚动,这时我们监听div的onscroll发现触发的时机区分android和ios两种情况,具体可以看下面表格:| 机型(内核) | body滚动 | 局部滚动 || :-: | :-: | :-: | | ios | 不能实时触发 | 不能实时触发 || android | 实时触发| 实时触发 || ios wkwebview内核 | 实时触发| 实时触发 |不能实时触发表现:只在手指触摸的屏幕上一直滑动时和滚动停止的那一刻才触发。关于模拟滚动概念正常的滚动:我们平时使用的scroll,包括上面讲的滚动都属于正常滚动,利用浏览器自身提供的滚动条来实现滚动,底层是由浏览器内核控制。模拟滚动:最典型的例子就是iscroll了,原理一般有两种:监听滚动元素的touchmove事件,当事件触发时修改元素的transform属性来实现元素的位移,让手指离开时触发touchend事件,然后采用requestanimationframe来在一个线型函数下不断的修改元素的transform来实现手指离开时的一段惯性滚动距离。监听滚动元素的touchmove事件,当事件触发时修改元素的transform属性来实现元素的位移,让手指离开时触发touchend事件,然后给元素一个css的animation,并设置好duration和function来实现手指离开时的一段惯性距离。方案比较第一种方案由于惯性滚动的时机时由js自己控制所以可以拿到滚动触发阶段的scrolltop值,并且滚动的回调函数onscroll在滚动的阶段都会触发。第二种方案相比第一种要劣势一些,区别在于手指离开时,采用的时css的animation来实现惯性滚动,所以无法直接触发惯性滚动过程中的onscroll事件,只有在animation结束时才可以借助animationend来获取到事件,当然也有一种方法可以实时获取滚动事件,也是借助于requestanimationframe来不断的去读取滚动元素的transform来拿到scrolltop同时触发onscroll回调。正常滚动和模拟滚动的性能比较模拟滚动的fps值波动较大,这样滚动起来会有明显的卡顿感觉,各位体验的时候如果滚动超过10屏之后就可以明显感觉到两着的区别。在使用模拟滚动时,浏览器在js层面会消耗更多的性能去改变dom元素的位置,在dom复杂层级深的页面更为高,所以在长列表滚动时还要使用正常滚动更好。滚动和下拉刷新方案1:借助iscroll的原理,整个页面使用模拟滚动,将下拉刷新元素放在顶部,当页面滚动到顶部下拉时,下拉刷新元素随着页面的滚动出现,当手指离开时收回,此方案实现起来较为简单直接借助iscoll即可,但是使用了模拟滚动之后在正常的列表滚动时性能上不如正常滚动。方案2:页面使用正常滚动,将下拉刷新元素放置在顶部top值为负值(正常情况下不可见),当页面处于顶部时下拉,这时监听touchmove事件,修改scrollcontent的tranlateY值,同时修改下拉刷新元素的tranlateY值,将两者同时位移来将下拉刷新元素显示出来,手指离开时(touchend)收回,这种方案满足了在正常列表滚动时使用原生的滚动节省性能,只在下拉刷新时使用模拟滚动来实现效果。方案3:方案2的改良版,唯一不同是将下拉刷新元素和scrollcontent放在一个div里,将下拉刷新元素的margintop设为负值,在下拉刷新时,只需要修改scrollcontent一个元素的tranlateY值即可实现下拉,在性能上要比方案2好。还会有一个性能上的问题就是:当页面的列表过长,dom元素过多时,在模拟滚动,下拉刷新这段时间内,页面也会有卡顿现象,这里采取了一个优化策略即:列表较长时dom数量较多时,在触发下拉刷新的时机时将页面视窗之外的dom元素隐藏或者存放在fragment里面。在刷新完成之后手指离开(touchend)时将隐藏的元素显示出来。需要注意的是,隐藏和显示视窗外的元素这个操作在下拉刷新时只会执行一次,并且只有在下拉刷新时才会执行。下面介绍如何去优化scroll事件的触发,避免scroll事件过度消耗资源:防抖(Debouncing)和节流(Throttling)scroll 事件本身会触发页面的重新渲染,同时 scroll 事件的 handler 又会被高频度的触发, 因此事件的 handler 内部不应该有复杂操作,例如 DOM 操作就不应该放在事件处理中。特别是针对此类高频度触发事件问题(例如页面 scroll ,屏幕 resize,监听用户输入等)。防抖(Debouncing)防抖技术即是可以把多个顺序地调用合并成一次,也就是在一定时间内,规定事件被触发的次数。节流(Throttling)防抖函数确实不错,但是也存在问题,譬如图片的懒加载,我希望在下滑过程中图片不断的被加载出来,而不是只有当我停止下滑时候,图片才被加载出来。又或者下滑时候的数据的 ajax 请求加载也是同理。这个时候,我们希望即使页面在不断被滚动,但是滚动 handler 也可以以一定的频率被触发(譬如 250ms 触发一次),这类场景,就要用到另一种技巧,称为节流函数(throttling)。节流函数,只允许一个函数在 X 毫秒内执行一次。与防抖相比,节流函数最主要的不同在于它保证在 X 毫秒内至少执行一次我们希望触发的事件 handler。关于防抖动与节流,我的博客文章也有提及。使用rAF(requestAnimationFrame)触发滚动事件如果页面只需要兼容高版本浏览器或应用在移动端,又或者页面需要追求高精度的效果,那么可以使用浏览器的原生方法 rAF(requestAnimationFrame)。window.requestAnimationFrame() 这个方法是用来在页面重绘之前,通知浏览器调用一个指定的函数。这个方法接受一个函数为参,该函数会在重绘前调用。rAF 常用于 web 动画的制作,用于准确控制页面的帧刷新渲染,让动画效果更加流畅,当然它的作用不仅仅局限于动画制作,我们可以利用它的特性将它视为一个定时器。(当然它不是定时器)通常来说,rAF 被调用的频率是每秒 60 次,也就是 1000/60 ,触发频率大概是 16.7ms 。(当执行复杂操作时,当它发现无法维持 60fps 的频率时,它会把频率降低到 30fps 来保持帧数的稳定。)var ticking = false; // rAF 触发锁 function onScroll(){ if(!ticking) { requestAnimationFrame(realFunc); ticking = true; }} function realFunc(){ // do something… console.log(“Success”); ticking = false;}// 滚动事件监听window.addEventListener(‘scroll’, onScroll, false);实现以16.7ms 触发一次 handler,降低了可控性,但是提升了性能和精确度。从本质上而言,我们应该尽量去精简 scroll 事件的 handler ,将一些变量的初始化、不依赖于滚动位置变化的计算等都应当在 scroll 事件外提前就绪。避免在scroll 事件中修改样式属性 / 将样式操作从 scroll 事件中剥离输入事件处理函数,比如 scroll / touch 事件的处理,都会在 requestAnimationFrame 之前被调用执行。因此,如果你在 scroll 事件的处理函数中做了修改样式属性的操作,那么这些操作会被浏览器暂存起来。然后在调用 requestAnimationFrame 的时候,如果你在一开始做了读取样式属性的操作,那么这将会导致触发浏览器的强制同步布局。滑动过程中尝试使用 pointer-events: none 禁止鼠标事件pointer-events 是一个 CSS 属性,可以有多个不同的值,大概的意思就是禁止鼠标行为,应用了该属性后,譬如鼠标点击,hover 等功能都将失效,即是元素不会成为鼠标事件的 target。pointer-events: none 可用来提高滚动时的帧频。的确,当滚动时,鼠标悬停在某些元素上,则触发其上的 hover 效果,然而这些影响通常不被用户注意,并多半导致滚动出现问题。对 body 元素应用 pointer-events: none ,禁用了包括 hover 在内的鼠标事件,从而提高滚动性能。大概的做法就是在页面滚动的时候, 给 添加上 .disable-hover 样式,那么在滚动停止之前, 所有鼠标事件都将被禁止。当滚动结束之后,再移除该属性。// css 代码.disable-hover,.disable-hover * { pointer-events: none !important;}// js 代码var body = document.body, timer;window.addEventListener(‘scroll’, function() { clearTimeout(timer); if(!body.classList.contains(‘disable-hover’)) { body.classList.add(‘disable-hover’) } timer = setTimeout(function(){ body.classList.remove(‘disable-hover’) },500);}, false);参考 移动 Web 的滚动,高性能滚动及页面渲染优化 ...

January 5, 2019 · 1 min · jiezi

主流移动滚动插件分析以及思路方案拓展(原生onscroll 事件实现的滚动加载不逊色)

背景现在流行的各种移动端滚动加载、上拉刷新组件,滑动越来越流畅,体验很好;例如 better-scroll、vue-infinite-scroll、iscroll、vue-scroll 等等比较好的方案去解决业务上的问题,可是笔者总会听到使用过程中也会产生一些奇怪的问题,或许会引起很多人的共鸣比如很多 better-scroll 小白会在一开始发现不能滚动,或者滚动加载异步数据better-scroll页面怎么不能滚动滚动加载了重复的数据(分页)、明明下面有数据,却拉不动等等各种姿势不对。笔者曾经已入坑了vue-infinite-scroll 和vue-scrollvue-infinite-scroll 是利用原生scroll 滚动,优点是原生滚动可以在列表加载了很多的时候不会卡顿,(黑科技)ios在微信上点两下回到顶部,但是不能滑动加速,插件本身不支持下拉刷新,而且在v-if 的组件下用到滚动比如一个侧边栏,会报错$mount error , 发现原因是我组件还是隐藏的时候,插件默认给我去跑$mount 钩子 ,我是通过改源码解决的,好坑,不过官方还是很快地发现了这个bug, 所以我觉得仍然是比较靠谱的vue-scroller 坑比较多,现在好像已经好久没维护了,不知道大家有无遇到过, 在同一个页面用vue-scroll 然后弹窗要 textarea 输入,超过行数产生滚动条,但坑爹的是textarea 竟然不能滚动了,看源码发现是touchMove 的事件里禁止了原生滚动,提了链接issue,我只能手动改源码了, 有图有真相对于滚动插件的实现原理以及优缺点对better-scroll、vue-scroll 这类滚动插件是要父元素container 定位在body, 禁止原生滚动,然后通过touch 事件,改变transform: translateY去实现,然后 refresh 去更新模拟滚动条长度对于 vue-infinite-scroll 这一类是通过原生onscroll 事件,判断scrollTop到达底部触发loadMore 加载异步数据, 当然原生的scroll 的缺点是 滚动点透,比如说在body上有一个弹窗滚动,滚动到底部之后会发现body 在滚了;第二个问题是,在finger触摸滚动而未结束滚动时,如果要做一些动作会有延时,不能像touchMove、touchEnd 那么灵敏我也是看了这篇文章得到了启发知识点1:移动web滚动问题在移动端,使用滚动来处理业务逻辑的情况有很多,例如列表的滚动加载数据,下拉刷新等等都需要利用滚动的相关知识,但是滚动事件在不同的移动端机型却又有不同的表现,下面就来一一总结一下。滚动事件:即onscroll事件,形成原因通俗解释是当子元素的高度超过父元素的高度时且父元素的高度时定值window除外,就会形成滚动条,滚动分为两种:局部滚动和body滚动。onscroll方法: 一般情况下当我们需要监听一个滚动事件时通常会用到onscroll方法来监听滚动事件的触发。如果在浏览器上调试这个方法在浏览器上很好用,但是如果跑在手机端就没有想象中的效果了。body滚动:在移动端如果使用body滚动,意思就是页面的高度由内容自动撑大,body自然形成滚动条,这时我们监听window.onscroll,发现onscroll并没有实时触发,只在手指触摸的屏幕上一直滑动时和滚动停止的那一刻才触发,采用了wk内核的webview除外。body滚动局部滚动 局部滚动:在移动端如果使用局部滚动,意思就是我们的滚动在一个固定宽高的div内触发,将该div设置成overflow:scroll/auto;来形成div内部的滚动,这时我们监听div的onscroll发现触发的时机区分android和ios两种情况,具体可以看下面表格:不同机型onscroll事件触发情况:body滚动 局部滚动ios 不能实时触发 不能实时触发android 实时触发 实时触发ios wkwebview内核 实时触发 实时触发wkwebview内核:这里说明一下关于ios的wkwebview内核是ios从ios8开始提供的新型webview内核,和之前的uiwebview相比,性能要好,具体大家可以自行查看关于wkwebview的相关概念。body滚动和局部滚动demo:这里我需要指出的是在采用wkwebview内核的页面中scroll是可以实时触发的,如果使用的是原本的uiwebview则不能够实时触发,手q目前使用的是uiwebview而新版微信使用的是wkwebview,大家可以分别使用来尝试一下下面的demo:局部滚动body滚动分别用ios手q和微信和android手q体验会有不同的结果。知识点2:关于模拟滚动有了上面介绍的关于滚动的知识,理解的模拟滚动就不难了。正常的滚动:我们平时使用的scroll,包括上面讲的滚动都属于正常滚动,利用浏览器自身提供的滚动条来实现滚动,底层是由浏览器内核控制。模拟滚动:最典型的例子就是iscroll了,原理一般有两种:1). 监听滚动元素的touchmove事件,当事件触发时修改元素的transform属性来实现元素的位移,让手指离开时触发touchend事件,然后采用requestanimationframe来在一个线型函数下不断的修改元素的transform来实现手指离开时的一段惯性滚动距离。2).监听滚动元素的touchmove事件,当事件触发时修改元素的transform属性来实现元素的位移,让手指离开时触发touchend事件,然后给元素一个css的animation,并设置好duration和function来实现手指离开时的一段惯性距离。这两种方案对比起来各有好处,第一种方案由于惯性滚动的时机时由js自己控制所以可以拿到滚动触发阶段的scrolltop值,并且滚动的回调函数onscroll在滚动的阶段都会触发。第二种方案相比第一种要劣势一些,区别在于手指离开时,采用的时css的animation来实现惯性滚动,所以无法直接触发惯性滚动过程中的onscroll事件,只有在animation结束时才可以借助animationend来获取到事件,当然也有一种方法可以实时获取滚动事件,也是借助于requestanimationframe来不断的去读取滚动元素的transform来拿到scrolltop同时触发onscroll回调。模拟滚动的fps值波动较大,这样滚动起来会有明显的卡顿感觉,各位体验的时候如果滚动超过10屏之后就可以明显感觉到两着的区别。在使用模拟滚动时,浏览器在js层面会消耗更多的性能去改变dom元素的位置,在dom复杂层级深的页面更为高,所以在长列表滚动时还要使用正常滚动更好。知识点3:滚动和下拉刷新下拉刷新的元素在页面顶部,正常浏览时不可见的。当在页面顶部往下滚动时出现下拉刷新元素,当手指离开时收起。以上两点时实现一个下拉刷新组件的基本步骤,结合我们上述关于滚动的描述,我们可以这样实现下拉刷新:方案1:借助iscroll的原理,整个页面使用模拟滚动,将下拉刷新元素放在顶部,当页面滚动到顶部下拉时,下拉刷新元素随着页面的滚动出现,当手指离开时收回,此方案实现起来较为简单直接借助iscoll即可,但是使用了模拟滚动之后在正常的列表滚动时性能上不如正常滚动。方案2:页面使用正常滚动,将下拉刷新元素放置在顶部top值为负值(正常情况下不可见),当页面处于顶部时下拉,这时监听touchmove事件,修改scrollcontent的tranlateY值,同时修改下拉刷新元素的tranlateY值,将两者同时位移来将下拉刷新元素显示出来,手指离开时(touchend)收回,这种方案满足了在正常列表滚动时使用原生的滚动节省性能,只在下拉刷新时使用模拟滚动来实现效果。方案3:方案2的改良版,唯一不同是将下拉刷新元素和scrollcontent放在一个div里,将下拉刷新元素的margintop设为负值,在下拉刷新时,只需要修改scrollcontent一个元素的tranlateY值即可实现下拉,在性能上要比方案2好。在采用了上述方案之后,还会有一个性能上的问题就是:当页面的列表过长,dom元素过多时,在模拟滚动,下拉刷新这段时间内,页面也会有卡顿现象,这里采取了一个优化策略即:1) 列表较长时dom数量较多时,在触发下拉刷新的时机时将页面视窗之外的dom元素隐藏或者存放在fragment里面。2) 在刷新完成之后手指离开(touchend)时将隐藏的元素显示出来。3) 需要注意的是,隐藏和显示视窗外的元素这个操作在下拉刷新时只会执行一次,并且只有在下拉刷新时才会执行。看完这篇文章,会感觉 better-scroll 也没那么牛逼了自己的看法滚动加载、上拉刷新一直是个前端界内一直在解决的题目,最近越发感觉自己要有自己的滚动插件,一个是用别人的东西不顺手,有问题只能在百度碰运气,解决不了问题可能要换框架或者改源码,时间也是挺耗的我还是喜欢 原生的onscroll 滚动加载,自己现在也是用这个实现的滚动加载,屡试不爽 在大神面前惭愧地贴下代码进入页面时 document.addEventListener(“scroll”, this.isToBottom);离开页面时 document.removeEventListener(“scroll”, this.isToBottom);敬请期待现在笔者正开发一个用两百行代码就可以实现原生onscroll滚动加载、下拉刷新,能够两次点击回到顶部、尽力打造稳定、解决问题、兼容性好、入手快、维护简单、源码简易的一个插件,区别于better-scroll 这种流畅感较好的插件,也算是一个 符合大众的一个解决方案,没有最好的只有最适合自己的

November 17, 2018 · 1 min · jiezi