函数去抖与函数节流
笔者:林大
有任何疑问欢迎关注微信公众号:网易游戏运维平台。(长按识别上图二维码)
微信公众号原文链接:函数去抖与函数节流
摘要
本文介绍两种用于控制前端 UI 事件触发频率的手段:函数去抖(Debounce)和函数节流(Throttle)。
背景
函数去抖(Debounce)和函数节流(Throttle)是限制一个函数的触发频率的两种手段,它的使用场景通常是在 Web 前端用于控制 DOM 事件和向后台发起请求的频率,以到达前端性能优化的目的。
前端时常会有高频率 UI 事件触发的场景,如鼠标的 scroll
事件:
图中的数字是 scroll
事件的触发次数,可以看到短短几秒内 scroll
事件就可以触发将近一百次,如果把类似「下拉加载更多」这种操作直接不加限制绑定到 scroll
事件上的话,性能将会损耗巨大,导致用户不可忍受的响应速度。
再看另一个具体的例子,以下是在网易 SRE 部门专用的移动运维 app —— SABox 中查询某个 SRE 的联系信息,每当输入框的内容发生改变时,就会触发 onchage
事件,改变人员信息列表的渲染内容,同时在旁边打印出那个时刻的输入框内容。可以看到在 ios 模拟器中列表的渲染已经有点延迟,如果在配置较低的安卓机,响应速度将会更慢。
因此,限制 UI 事件的触发频率是非常有必要的,而函数去抖和函数节流这两个手段的原理也非常简单。
函数去抖(Debounce)
简单的一句话描述 Debounce 就是「将一段时间内若干次函数调用聚合成一次」。举一个比较不恰当的例子:坐电梯的时候,把「电梯启动」看作函数真正执行,把「有人进入电梯」看作函数调用,电梯会在等待一段时间之后就会关门启动,当电梯门准备关上时,有人进来了,然后电梯又需要等待固定的一段时间,直到这段时间内没有其他人再进入电梯,最终才启动。
Debounce的定义 :当执行函数一段时间之后,才会允许下一次执行,若在这段时间内又调用此函数则将重新计时。
上图演示了 Debounce 的过程:不断点击长方形,并不会让函数真正执行,直到短暂的中止点击之后,函数被真正执行,这就是「将一段时间内若干次函数调用聚合成一次」。
下面是 Debounce 的简单实现:
var debounce = function(fn, delay) {
var timer = null;
return function() {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
};
window.onresize = debounce(myFunc, 100);
核心是定时器。当事件触发时,利用 setTimout
让其延迟执行,如果在这个延迟的时间间隔内又触发了事件,则重新计时(clearTimeout
)。
Debounce 的应用场景非常多:输入框的各种事件(表单校验、ajax请求)、拖动浏览器窗口大小时的 resize
事件等,下面是一个窗口 resize
的例子:
调用的函数是把当前窗口的尺寸打印出来,左边的每次 resize
都触发了,右边的只有在松开鼠标(停止 resize
)之后才触发,节省了大量的函数调用。
再用回 SABox 人员列表的例子,只有在输入的间隔大于一定时间,才触发列表的渲染事件:
Debounce 还能扩展出更多的特性,例如 leading
标志,设定 leading = true
可以让事件的触发马上发生,而不是等到聚合结束:
函数节流(Throttle)
Throttle 更接近「限制频率」的思路,它预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期。换句话说,如果我规定执行周期是1秒,则这1秒内最多只能调用一次这个函数,要想再调用必须等到下1秒。
在下拉加载、拖拽等场景中,Debounce 并不适用,因为如果这些动作用户一直持续在做,就永远等不到事件真正触发的那个时刻,在这种情况下,我们需要用 Throttle ,保证在一个周期内,事件被执行一次。
上图演示了 Throttle 的过程,在往下滚动页面时,限制某个周期内计算一次与页面底端的距离,一旦小于某个值就触发「加载更多」。如果这里使用了 Debounce,则必须等到滚动结束才会触发「加载更多」,换言之一直滚动就触发不了。
下面是 Throttle 的简单实现:
var throttle = function(fn, delay) {
var last = 0;
return function() {
var curr = new Date();
if (curr - last > delay) {
fn.apply(this, arguments);
last = curr;
}
}
}
window.onresize = throttle(myFunc, 100);
核心原理是每次事件触发时都比对与上次事件触发的时间间隔,如果大于所需的时间间隔,则允许触发,然后记录下新的触发时间,用于下一次比对。
Lodash 中的 Debounce 和 Throttle
Lodash 是一个非常实用的 JavaScript 工具库,它里面已经集成了 Debounce 和 Throttle 两种功能,功能完善。
/**
* @param {Function} func 待调用的函数
* @param {number} [wait=0] 时间间隔(毫秒)
* @param {Object} [options={}]
* @param {boolean} [options.leading=false]
* leading 标志:是否在一开始就触发
* @param {number} [options.maxWait]
* 最大等待时间(毫秒)
* @param {boolean} [options.trailing=true]
* trailing 标志:是否在结束后触发
* @returns {Function} 返回经过 Debounce 处理的函数
*/
_.debounce(func, [wait=0], [options={}])
/**
* @param {Function} func 待调用的函数
* @param {number} [wait=0] 时间间隔(毫秒)
* @param {Object} [options={}]
* @param {boolean} [options.leading=true]
* leading 标志:是否在一开始触发
* @param {boolean} [options.trailing=true]
* trailing 标志:是否在结束后触发
* @returns {Function} 返回经过 Throttle 处理的函数
*/
_.throttle(func, [wait=0], [options={}])
而 lodash throttle 的源码里很有趣的是:实际上它返回了一个传入了 options.maxWait = wait
的 debounce 函数。短短的源码不超过20行:
function throttle(func, wait, options) {
let leading = true
let trailing = true
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading
trailing = 'trailing' in options ? !!options.trailing : trailing
}
return debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
})
}
export default throttle
总结
Debounce 和 Throttle 都是限制函数调用次数的手段,本文仅介绍了它们在 Web 前端的 UI 性能优化中的应用;Debounce 适用于输入事件、窗口 resize 等频繁进行但穿插较长间隔的场景, Throttle 适用于拖拽、下拉加载更多等必须在一个周期里要执行一次的场景。但 Debounce 和 Throttle 的应用不仅仅局限于前端 和 JavaScript,其他语言、服务器端在需要限制执行频率的时候,也可借鉴其思想。