前情提要: 在 为何 iOS 的 scroll 阻塞 dom 机制没被大规模借鉴? 中提到,iOS 的 UIWebView 有个特性,页面滚动时会阻塞 js 的 scroll 事件触发和 css 的页面重绘,大大提高了滚动性能。本质上说,这是在前端开发者之外,在浏览器上实现的 scroll 防抖优化,哪怕程序员并不需要(破坏一致性)。
作为用户我是需要的,因为有些人写的代码实在不靠谱。我要在桌面浏览器上实现同样的功能,即对那些包含糟糕滚动性能的页面,统一加上 scroll 防抖处理:对一连串 scroll 动作,只在最后触发一个 scroll 事件,哪怕开发者没做任何防抖和节流。
在 Opera 12 浏览器中,存在一个全局 Opera 对象,提供一些堪称魔法的函数,让用户对网页事件流做细致的调整,举个例子:
有些网页会监听用户的 copy 动作,显示一个难看的提示框,我们这么处理
opera.addEventListener('BeforeEventListener.copy',function(e){
e.preventDefault();
});
哪怕不知道回调函数,对 copy 事件的监听也被去除了,这个方法是本文的核心。
为了观察 scroll 是否完全停止,用一个全局变量保存 scroll 事件的时间戳。
window.lastScroll = new Date();
为了即使自己的其他代码禁用 scroll 事件,也可以正确的记录时间戳,同样使用 Opera 对象
opera.addEventListener('AfterEvent.scroll', function (e){
window.lastScroll = e.timeStamp ;
});
即使 scroll 事件不触发,Opera 也可以捕获 AfterEvent.scroll 事件,时间戳是一样的。
下面对 scroll 事件做防抖,不要介意我的渣代码
opera.addEventListener('BeforeEventListener.scroll',function(e){
// 阻止事件触发,禁用原有的回调
e.preventDefault();
var callee = arguments.callee ;
// timer 为事件墙,只让第一个 scroll 事件通过,其他废弃
if( callee.timer )
return;
// 首次滚动立即延迟,观察有无后续滚动
// 同时记录时间戳
callee.timer = setTimeout( function(){
// 高于阈值,滚动完全结束,进入回调组装流程
if( new Date() - window.lastScroll > 800 )
{
// 清理事件墙
callee.timer = null ;
// 提取原来的回调函数 e.listener
// 提取原来的 scroll 事件 e.event
// 组合并触发回调
e.listener(e.event);
} else {
// 低于阈值,滚动未结束,继续延迟回调
// 同时更新时间戳
callee.timer = setTimeout( arguments.callee , 400 );
}
} , 400 );
});
这段代码的逻辑是,从开发者对 scroll 的监听过程捕获 BeforeEventListener.scroll 事件,提取其中的 scroll 事件( e.event ),回调函数( e.listener ),按照一定的逻辑重新组装到一起,或者立即触发,或者延迟执行。
在延迟的过程中,对一串 scroll 事件,只在滚动停止后触发一次。
至此,借助浏览器提供的专有对象,实现了 scroll 事件的系统级防抖处理,滚动体验被大大提升。在我有限的认知里,没有其他浏览器有这么强的内置功能,可以实现类似的效果。如果 Chrome 或者 Firefox 能够做到,请留言告诉我。