虽然这个问题有点老了,但我还是想回答一下,因为我经常看到很多脚本滥用这些技术。
总的来说,你提到的工具(rAF、rIC和被动监听器)都是非常好的工具,并且不会很快消失。但你必须知道为什么要使用它们。
在开始之前,如果你生成了与滚动同步/滚动链接效果相关的特效,比如视差效果/粘性元素,使用rIC、setTimeout进行节流是没有意义的,因为你希望立即做出反应。
requestAnimationFrame
rAF
在浏览器计算文档的新样式和布局之前,提供了帧生命周期内的一个时间点。这就是为什么它非常适合用于动画的原因。首先,它不会比浏览器计算布局的频率更频繁或更少频繁(正确的频率)。其次,它在浏览器计算布局之前被调用(正确的时机)。事实上,对于任何布局更改(DOM或CSSOM更改),使用rAF
都是有意义的。与浏览器中其他与布局渲染相关的内容一样,rAF
与V-SYNC同步。
使用rAF
进行节流/防抖
Paul Lewis的默认示例如下:
var scheduledAnimationFrame;
function readAndUpdatePage(){
console.log('read and update');
scheduledAnimationFrame = false;
}
function onScroll (evt) {
lastScrollY = window.scrollY;
if (scheduledAnimationFrame){
return;
}
scheduledAnimationFrame = true;
requestAnimationFrame(readAndUpdatePage);
}
window.addEventListener('scroll', onScroll);
这个模式在实践中很常用/复制,尽管它在实际中几乎没有意义。 (我在想为什么没有开发者看到这个明显的问题。)从理论上讲,将所有内容至少限制到
rAF
是有道理的,因为请求浏览器进行布局更改的频率不应该超过浏览器渲染布局的频率。
然而,每次浏览器
渲染滚动位置变化时,都会触发
scroll
事件。这意味着
scroll
事件与页面的渲染同步。这与
rAF
提供的功能完全相同。这意味着通过已经被定义为相同事物进行限制是没有任何意义的。
实际上,您可以通过添加
console.log
来验证我刚才说的话,并检查这个模式如何“防止多个rAF回调”(答案是没有,否则这将是浏览器的错误)。
if (scheduledAnimationFrame){
console.log('prevented rAF callback');
return;
}
如您所见,这段代码从未被执行过,它只是死代码。
但是有一个非常相似的模式,出于不同的原因而有意义。它看起来像这样:
function writeLayout(){
element.classList.add('is-foo');
}
window.addEventListener('scroll', ()=> {
box = element.getBoundingClientRect();
if(box.top > pos){
requestAnimationFrame(writeLayout);
}
});
使用这种模式,您可以成功地减少甚至消除布局抖动。其思想很简单:在滚动监听器中,您读取布局并决定是否需要修改DOM,然后使用rAF调用修改DOM的函数。为什么这有帮助呢?
rAF
确保您将布局无效化(在帧末)。这意味着同一帧内调用的任何其他代码都可以在有效的布局上运行,并且可以使用超快的布局读取方法进行操作。
事实上,这种模式非常好,我建议使用以下辅助方法(使用ES5编写):
function bindRaf(fn, throttle) {
var isRunning;
var that;
var args;
var run = function() {
isRunning = false;
fn.apply(that, args);
};
return function() {
that = this;
args = arguments;
if (isRunning && throttle) {
return;
}
isRunning = true;
requestAnimationFrame(run);
};
}
requestIdleCallback
这个API类似于rAF
,但提供了完全不同的功能。它在帧内部提供了一些空闲时间。(通常是在浏览器计算布局和完成绘制之后,但在垂直同步之前还有一些时间。)即使页面在用户视图中出现延迟,也可能存在浏览器处于空闲状态的帧。尽管rIC
可以提供最多50毫秒的时间,但大部分时间你只有0.5到10毫秒来完成任务。由于rIC
回调函数在帧生命周期的哪个阶段被调用,你不应该修改DOM(使用rAF
来实现)。
最后,使用rIC
来限制scroll
监听器对于懒加载、无限滚动等功能非常有意义。对于这些类型的用户界面,甚至可以更加限制并在其前面添加一个setTimeout
。(即等待100毫秒,然后再执行rIC
)
这里有
debounce和
throttle的实际例子。
还有{{link3:一篇关于
rAF
的文章},其中包含两个图表,可以帮助理解“帧生命周期”中的不同点。
被动事件监听器。
被动事件监听器的发明是为了提高滚动性能。现代浏览器将页面滚动(滚动渲染)从主线程移动到合成线程。(参见
https://hacks.mozilla.org/2016/02/smoother-scrolling-in-firefox-46-with-apz/)
但是,有些事件会产生滚动,可以通过脚本来阻止(这在主线程中发生,因此可能会逆转性能改进)。
这意味着一旦绑定了其中一个事件监听器,浏览器就必须等待这些监听器执行完毕,然后才能计算滚动。这些事件主要包括
touchstart
、
touchmove
、
touchend
、
wheel
,理论上还包括
keypress
和
keydown
。而
scroll
事件本身
不是这些事件之一。
scroll
事件没有默认的可以被脚本阻止的动作。
这意味着如果你在
touchstart
、
touchmove
、
touchend
和/或
wheel
事件中不使用
preventDefault
,请始终使用被动事件监听器,那么一切都应该没问题。
如果你使用了preventDefault,请检查是否可以用CSS的
touch-action
属性替代它,或者至少在DOM树中降低它的优先级(例如对于这些事件不进行事件委托)。对于
wheel
事件监听器,你可能可以在
mouseenter
/
mouseleave
上绑定/解绑它们。
对于其他任何事件:使用被动事件监听器来提高性能是没有意义的。最重要的是要注意:
scroll
事件无法被取消,因此使用被动事件监听器来处理scroll
事件从来都没有意义。
对于无限滚动视图,你不需要
touchmove
,只需要
scroll
,所以被动事件监听器甚至不适用。
总结
回答你的问题:
- 对于懒加载和无限滚动,使用
setTimeout
+ requestIdleCallback
的组合来处理事件监听器,并使用rAF
来处理任何布局写入(DOM变动)。
- 对于即时效果,仍然使用
rAF
来处理任何布局写入(DOM变动)。