在性能优化实践中,遇到scroll事件,优先需要考虑节流。 节流函数顾名思义,就是要使密集频繁触发的函数,按照有规律有节制的过程去执行,那么背后的原理是什么?一步步实现吧。
1 最最最简单使用场景举例:监听屏幕滚动,添加回调函数,要怎么写呢?
var num = 0
window.onscroll = function () {
console.log('回调函数执行' + (++num) + '次')
}
效果如下
可以发现,随意滚动屏幕,回调函数就会执行很多次,也就是频繁的触发着回调函数,所以为了减少性能开销,要降低函数执行频率。
2 如果只让滚动事件结束后触发回调函数,该怎么做?
var num = 0
var timer = null
var cb = function(){
console.log('回调函数执行' + (++num) + '次')
}
window.onscroll = function () {
clearTimeout(timer)
timer = setTimeout(() => {
cb()
}, 1000)
}
可以看到结果是,在页面滚动结束的一秒后,控制台才会打印结果。
其实window绑定的滚动监听事件在页面滚动时候还是和上面一样的触发。只是用来控制台打印的回调函数,放在setTimeout中去执行。
滚动监听函数触发间隔远远小于setTimeout的一秒,所以回调函数cb还未来得及执行,就已经被下一次的滚动clearTimeout(timer)了。
只有在滚动结束的前一次触发,会生成一个setTimeout不会再被清除,也就在滚动结束后一秒执行了cb函数。
那么这么写有什么问题存在? timer直接被定义在了全局,应该避免。
var num = 0
var cb = function(){
console.log('回调函数执行' + (++num) + '次')
}
var throttle = function (fn, delay) {
var timer = null
return function () {
clearTimeout(timer);
timer = setTimeout(function() {
fn();
}, delay);
}
}
window.onscroll = throttle(cb, 1000)
为什么这么写可以避免全局变量污染?仔细看,就是一个闭包。
window.onscroll 绑定的是 throttle(cb, 1000) ,即绑定的是传入参数为cb 和 1000,throttle执行后返回的函数。这个函数可以一直拿到父级作用域下的timer,而在window环境下无法拿到这个timer变量。也就避免了全局变量污染。
到目前为止,还只是解决了在滚动结束后触发回调函数这个问题。而这并不是节流。因为上述写法在不间断的滚动屏幕过程中,不会执行回调函数,只有在停止滚动后才会去执行。这显然不是节流。那么需要在滚动过程中,至少400ms就要触发一次传入的回调函数,要如何修改?
var num = 0
var cb = function(){
console.log('回调函数执行' + (++num) + '次')
}
var throttle = function (fn, delay, atleast) {
// 声明定时器
var timer = null
var previous = null
return function () {
// 每次页面滚动时候都会生成新的时间戳
var now = +new Date()
// 如果是第一次滚动
if ( !previous ) previous = now
// 不断的滚动,直到时间间隔满足条件,执行回调函数,更新previous,清空定时器
if ( atleast && now - previous > atleast ) {
fn();
previous = now
clearTimeout(timer)
// 不满足时间间隔条件,还是进函数中先清空上次的定时器
// 并生成新的定时器,
} else {
clearTimeout(timer)
timer = setTimeout(function() {
fn()
}, delay);
}
}
}
window.onscroll = throttle(cb, 1000, 400)