一、目的
以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃:
- window对象的resize、scroll事件
- 拖拽时的mousemove事件
- 射击游戏中的mousedown、keydown事件
- 文字输入、自动完成的keyup事件
对于window的resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。
针对这两种需求就出现了throttle(又称节流)和debounce(又称去抖)两种解决办法,用来减少调用频率,同时又不影响实际效果。
二、函数防抖
函数防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次;如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
如下图,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件。
例:实现简单的debounce
// fn是我们需要包装的事件回调, delay是每次推迟执行的等待时间
function debounce(fn, delay) {
// 定时器
let timer = null;
// 将debounce处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this;
// 保留调用时传入的参数
let args = arguments;
// 每次事件被触发时,都去清除之前的旧定时器
if(timer!==null) {
clearTimeout(timer);
}
// 设立新定时器
timer = setTimeout(function () {
fn.apply(context, args);
}, delay)
}
}
// 用debounce来包装scroll的回调
const better_scroll = debounce(function(){
console.log('触发了滚动事件');
}, 1000);
document.addEventListener('scroll', better_scroll);
三、函数节流
函数节流(throttle):当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
节流通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。
如下图,持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle。
函数节流主要有两种实现方法:时间戳和定时器。
例:使用时间戳实现节流
// fn是我们需要包装的事件回调, interval是时间间隔的阈值
function throttle(fn, interval) {
// last为上一次触发回调的时间
let prev=Date.now();
// 将throttle处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this;
// 保留调用时传入的参数
let args = arguments;
// 记录本次触发回调的时间
let now = Date.now();
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
if (now - prev >= interval) {
// 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
prev = now;
fn.apply(context, args);
}
}
}
// 用throttle来包装scroll的回调
const better_scroll = throttle(function(){
console.log('触发了滚动事件');
}, 1000);
document.addEventListener('scroll', better_scroll);
- 当高频事件触发时,第一次会立即执行(给scroll事件绑定函数与真正触发事件的间隔一般大于delay),而后频繁地触发事件,也都是每delay时间才执行一次;
- 当最后一次事件触发完毕后,事件也不会再被执行了 (最后一次触发事件与倒数第二次触发事件的间隔小于delay)。
例:使用定时器实现节流
var throttle = function(fn, delay) {
var timer = null;
return function() {
var context = this;
var args = arguments;
if (!timer) {
timer = setTimeout(function() {
fn.apply(context, args);
timer = null;
}, delay);
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
- 当触发事件时,设置一个定时器,再次触发事件的时候,如果定时器存在,就不执行,直到delay时间后,定时器执行函数,并且清空定时器,这样就可以设置下个定时器。
- 当第一次触发事件时,不会立即执行函数,而是在delay秒后才执行。而后再怎么频繁触发事件,也都是每delay时间才执行一次;当最后一次停止触发后,由于定时器的delay延迟,可能还会执行一次函数。
例:使用时间戳+定时器实现节流
var throttle = function(func, delay) {
var timer = null;
var startTime = Date.now();
return function() {
var curTime = Date.now();
var remaining = delay - (curTime - startTime);
var context = this;
var args = arguments;
clearTimeout(timer);
if (remaining <= 0) {
func.apply(context, args);
startTime = Date.now();
}
else {
timer = setTimeout(func, remaining);
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
- 在节流函数内部使用开始时间startTime、当前时间curTime与delay来计算剩余时间remaining;
- 当remaining<=0时表示该执行事件处理函数了(保证了第一次触发事件就能立即执行事件处理函数和每隔delay时间执行一次事件处理函数);
- 如果还没到时间的话就设定在remaining时间后再触发 (保证了最后一次触发事件后还能再执行一次事件处理函数);
- 当然在remaining这段时间中如果又一次触发事件,那么会取消当前的计时器,并重新计算一个remaining来判断当前状态。
四、总结
-
函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
-
函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
-
区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
参考链接:mp.weixin.qq.com