浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制,这就出现了防抖和节流。
防抖(debounce)
( 防抖 ):短时间内,多次触发同一个函数,只会执行最后一次
(在规定的延迟时间内,如果再次触发函数,则会清掉上一次函数,重新触发一个新函数,不停地触发则会不停地清除上一次函数,直到停止操作,然后延迟时间之后,该函数被执行,延迟时间一般较短)
// 普通方案
window.addEventListener('resize', () => {
console.log('trigger');
})
// 会不停的触发resize函数,不停地输出trigger
在 resize 事件上绑定处理函数,这时 debounce 函数会立即调用,实际上绑定的函数时 debounce 函数内部返回的函数(这点很重要)
每一次事件被触发,都会清除当前的 timer 然后重新设置超时调用,只有在最后一次触发事件,才能在 delay 时间后执行(实现防抖的原理)
// debounce 函数接受一个函数和延迟执行的时间作为参数
function debounce(fn, delay){
// 维护一个 timer
let timer = null;
return function() {
// 获取函数的作用域和变量
let context = this;
let args = arguments;
// clearTimeout是关键,在上次函数未被执行时,再次触发事件,会清除上次定时任务
clearTimeout(timer);
timer = setTimeout(function(){
fn.apply(context, args);
}, delay)
}
}
function foo() {
console.log('trigger');
}
// 在 debounce 中包装我们的函数,过 2 秒触发一次
window.addEventListener('resize', debounce(foo, 2000));
我们也可以为 debounce 函数加一个参数,可以选择是否立即执行函数
function debounce(func, delay, immediate){
var timer = null;
return function(){
var context = this;
var args = arguments;
if(timer) clearTimeout(timer);
if(immediate){
var doNow = !timer;
timer = setTimeout(function(){
timer = null;
},delay);
if(doNow){
func.apply(context,args);
}
}else{
timer = setTimeout(function(){
func.apply(context,args);
},delay);
}
}
}
节流(throttle)
( 节流 ):规定的延迟时间内,只允许函数执行一次
(规定的延迟时间内,无论做多少次操作都只执行一次函数)
(与防抖的不同是,防抖如果你一直操作,如mousemove,一直不会执行回调函数,只有当停下时,才会执行)
(节流是,在规定的延迟时间内,如2秒,无论你操作多少次,都只会执行一次函数,如果一直操作的话,那么每2秒会执行一次,不会像防抖一样,只有停下来才会执行)
应用场景如:输入框的联想,可以限定用户在输入时,只在每两秒钟响应一次联想。
时间戳实现:可以看出,时间戳实现节流第一次触发事件时,结果会在第一时间输出
// 节流
function throttle(func, delay) {
// 设置prev = 0为参考系
var prev = 0;
return function () {
var context = this;
var args = arguments;
var now = Date.now();
// 由于 now - 0 必大于 delay 所以,首次语句必定被执行
if (now - prev >= delay) {
func.apply(context, args);
// 一次执行完之后,将prev = 当前时间,这一步是实现节流的关键,使得函数在规定延迟时间内只能执行一次
prev = Date.now();
}
}
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi, 300));
定时器实现:可以看出,定时器实现节流第一次触发事件时,结果不会被第一时间输出
// 节流
function throttle(func, delay) {
// 初始化timer = false,可以令闭包函数中的定时器立马被执行
// 但是这里要注意,只是定时器被立即执行,因为定时器是延时的,所以定时器里面的代码在delay后执行
var timer = false;
return function () {
var context = this;
var args = arguments;
// !false就是true所以这里被立即执行了
if (!timer) {
// 这里是关键,将timer赋值成定时器,此时的timer为true
// !true就是false,所以不走if条件里面的代码,直到定时器里面的代码执行完毕
// timer被重新赋值为false
timer = setTimeout(function () {
func.apply(context, args);
timer = false;
}, delay);
}
}
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi, 300));
时间戳 和 定时器 实现节流 区别 在于:
使用 时间戳 实现的节流函数会在第一次触发事件时立即执行,以后每过 delay 秒之后才执行一次,并且最后一次触发事件不会被执行;
而 定时器 实现的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数。