初稿:2017-07-15
更新: 2018-07-28
函数节流与函数防抖
函数防抖一部分我是学习于《JavaScript 高级程序设计》第三版,但是文中标题错误标为函数节流,实则为函数防抖。
网上大部分文章也是节流[throttle]防抖[debounce]分不清楚,其实这两个名称是极其形象的。
throttle-函数节流:一个水龙头在滴水,可能一次性会滴很多滴,但是我们只希望它每隔 500ms 滴一滴水,保持这个频率。即我们希望函数在以一个可以接受的频率重复调用。
debounce-函数防抖:将一个弹簧按下,继续加压,继续按下,只会在最后放手的一瞬反弹。即我们希望函数只会调用一次,即使在这之前反复调用它,最终也只会调用一次而已。
希望大家看完全文能够返回来看一看这段总结。
函数防抖
为什么会有函数防抖
先看一段代码:
window.onresize = function() {
var div = document.getElementById('mydiv')
div.style.height = div.offsetWidth + 'px'
console.log('resize')
}
很明显,监听浏览器窗口的 resize 事件,并基于该事件改变页面布局。首先,计算 offsetWidth 属性,如果该元素或者页面上其他元素有非常复杂的 CSS 样式,那么这个过程将会很复杂。其次,设置某个元素的高度需要对页面进行回流来令改动生效。如果页面有很多元素同时应用了相当数量的 CSS 的话,这又需要很多运算,可以通过'resize'输出的次数来观察函数调用的次数。
浏览器中某些计算和处理要比其他的昂贵很多。例如,DOM 操作比起非 DOM 交互需要更多的内存和 CPU 时间。连续尝试进行过多的 DOM 相关操作可能会导致浏览器挂起,有时候甚至会崩溃。
很明显,用户如果不断放大缩小浏览器窗口,那我们监听函数将会不停的被调用,倘若函数过“重”,即假设如上文描述的一般,那么对浏览器的压力将会非常之大,其高频率的更改可能会让浏览器崩溃。
什么是函数防抖
但是你会发现在某些页面的时候,你手指按住鼠标左键不动拖拉扩大或缩小浏览器时,页面内部并没有同步变化,当你松开手指时,页面才重新根据浏览器窗口变化。
此处便用到了函数防抖的思想,不论用户如何放大缩小,在他结束的的那一刻才进行我们的方法调用即可,不论怎么按弹簧,放手的时候才回弹。
debounce-函数防抖:将一个弹簧按下,继续加压,继续按下,只会在最后放手的一瞬反弹。即我们希望函数只会调用一次,即使在这之前反复调用它,最终也只会调用一次而已。
基本思想
某些代码不可以在没有间断的情况下连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作(清除定时器)就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。
基本模式
摘自高级程序设计
var processor = {
timeoutId: null,
//实际进行处理的方法
performProcessing: function() {
//实际执行代码
},
//初始处理调用的方法
process: function() {
clearTimeout(this.timeoutId)
var that = this
this.timeoutId = setTimeout(function() {
that.performProcessing()
}, 100)
}
}
// 尝试开始执行
processor.process()
但是大部分人一般对下面这种使用方式更为熟悉;
function debounce(fn) {
var timer
var _self = fn
return function() {
clearTimeout(timer)
var args = arguments // fn所需要的参数
var _me = this // 当前的this
timer = setTimeout(function() {
_self.call(_me, args)
}, 200)
}
}
通过debounce(fn)
代替fn
;
debounce 函数接受要防抖的函数作为参数,返回值为一个匿名函数。
返回的匿名函数首先清除之前的定时器。定时器代码使用 call()来确保方法在适当的环境中执行。
解决问题
让我们回到最开始的代码:
window.onresize = function() {
var div = document.getElementById('mydiv')
div.style.height = div.offsetWidth + 'px'
console.log('resize')
}
现在看来这段代码弊端就很明显了,我们需要对这个函数防抖一下。让用户拉拉拖拖结束过后 200ms 再改变样式,节省浏览器资源。
function debounce(fn) {
var timer
var _self = fn
return function() {
clearTimeout(timer)
var args = arguments // fn所需要的参数
var _me = this // 当前的this
timer = setTimeout(function() {
_self.call(_me, args)
}, 200)
}
}
function resizeDiv() {
var div = document.getElementById('mydiv')
div.style.height = div.offsetWidth + 'px'
console.log('resize')
}
window.onresize = debounce(resizeDiv)
函数节流
为什么会有函数节流
基于上文的场景,我们完成了用户 resize 浏览器后 mydiv 的高度与宽度一致的这个需求。
这时候产品来了,说“你们这个有点奇怪呀,我放手之后他才变化,有点突兀,我在拖动的时候不能也让他变化一致吗?”
这就很尴尬了,好不容易想出来的防抖,就这样 pass 了?不,轮到节流登场了。
什么是函数节流
需求:优化用户体验,我们需要用户在 resize 浏览器窗口的过程中,height 与 width 也能保持一致,时刻触发函数肯定是不可以的,所以需要优化频率。
resize 过程中如果重复调用一个函数,让其以 500ms 的间隔执行,而非重复执行。
throttle-函数节流:一个水龙头在滴水,可能一次性会滴很多滴,但是我们只希望它每隔 500ms 滴一滴水,保持这个频率。即我们希望函数在以一个可以接受的频率重复调用。
基本模式
function throttle(fn, interval) {
var _self = fn // 保存需要被延迟执行的函数引用
var firstTime = true // 是否初次调用
var timer // 定时器
return function() {
var args = arguments
var _me = this
if (firstTime) {
// 如果是第一次调用不需要延迟执行
_self.call(_me, args)
firstTime = false
}
if (timer) {
// 如果定时器还在,说明前一次延迟执行还没有完成
return false
}
timer = setTimeout(function() {
// 延迟一段时间执行
clearTimeout(timer) // 清除定时器 避免下一次return false
timer = null
_self.call(_me, args)
}, interval || 500)
}
}
解决问题
function resizeDiv() {
var div = document.getElementById('mydiv')
div.style.height = div.offsetWidth + 'px'
console.log('resize')
}
window.onresize = throttle(resizeDiv)
现在我们即保证了用户体验也完成了性能保证。
参考书籍:《JavaScript 高级程序设计第三版》《JavaScript 设计模式与开发实践》