防抖(debounce)和节流(throttle)两词其实并非计算机领域的原生词语。追根溯源,防抖一词来自于弱电领域,指代的是消除外界对于开关扰动的技术。而节流来自于流体力学,指代的是限定流体流量的一种技术。由于JavaScript拥有事件驱动的特性,为避免事件频繁触发导致性能的损耗,防抖和节流这两种技术在JavaScript中亦被广泛应用。
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
理解:函数防抖就是法师发技能的时候要读条,技能读条没完再按技能就会重新读条。
这是高程中的经典代码:
function debounce(method, context) {
clearTimeout(method.tId);
method.tId = setTimeout(function () {
method.call(context);
}, 100)
}
实现 1
// 实现 1
// fn 是需要防抖处理的函数
// wait 是时间间隔
function debounce(fn, wait = 50) {
// 通过闭包缓存一个定时器 id
let timer = null
// 将 debounce 处理结果当作函数返回
// 触发事件回调时执行这个返回函数
return function(...args) {
// 如果已经设定过定时器就清空上一次的定时器
if (timer) clearTimeout(timer)
// 开始设定一个新的定时器,定时器结束后执行传入的函数 fn
timer = setTimeout(() => {
fn.apply(this, args)
}, wait)
}
}
// DEMO
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000)
// 停止滑动 1 秒后执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)
实现 2(立即执行)
上述实现方案已经可以解决大部分使用场景了,不过想要实现第一次触发回调事件就执行 fn 有点力不从心了,这时候我们来改写下 debounce 函数,加上第一次触发立即执行的功能。
function debounce(func, wait, scope, immediate = false) {
var timer = null
// (...args) 或者这里设置形参
return function() {
var context = scope || this,
args = arguments
var later = function() {
func.apply(context, args)
}
clearTimeout(timer)
// ------ 新增部分 start ------
// immediate 为 true 表示第一次触发后执行
// timer 为空表示首次触发
if (immediate && !timer) {
console.log('first call')
later()
}
// ------ 新增部分 end ------
timer = setTimeout(later, wait)
}
}
测试案例
//测试
function fn() {
console.log('fn')
}
setInterval(debounce(fn, 2000, true), 1900) // 更改定时器,即可查看效果
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
理解:函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。
// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
// 上一次执行 fn 的时间
let previous = 0
// 将 throttle 处理结果当作函数返回
return function(...args) {
// 获取当前时间,转换成时间戳,单位毫秒
let now = +new Date()
// 将当前时间和上一次执行函数的时间进行对比
// 大于等待时间就把 previous 设置为当前时间并执行函数 fn
if (now - previous > wait) {
previous = now
fn.apply(this, args)
}
}
}
function throttle(func, wait, scope) {
var timer = null
return function() {
var args = arguments
var context = scope || this
if (!timer) {
timer = setTimeout(function() {
timer = null
func.apply(context, args)
}, wait)
}
}
}
测试案例
//测试
function fn() {
console.log('fn')
}
setInterval(throttle(fn, 200), 20)
现在考虑一种情况,如果用户的操作非常频繁,不等设置的延迟时间结束就进行下次操作,会频繁的清除计时器并重新生成,所以函数 fn 一直都没办法执行,导致用户操作迟迟得不到响应。
有一种思想是将「节流」和「防抖」合二为一,变成加强版的节流函数,关键点在于「 wait 时间内,可以重新生成定时器,但只要 wait 的时间到了,必须给用户一个响应」。这种合体思路恰好可以解决上面提出的问题。
结合 throttle 和 debounce 代码,加强版节流函数 throttle 如下,新增逻辑在于当前触发时间和上次触发的时间差小于时间间隔时,设立一个新的定时器,相当于把 debounce 代码放在了小于时间间隔部分。
function throttle(func, wait, scope) {
wait || (wait = 250)
var timer = null,
last = 0
return function() {
var context = scope || this
var now = +new Date(),
args = arguments
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔
if (now - last < wait) {
if (timer) clearTimeout(timer)
timer = setTimeout(function() {
last = now
func.apply(context, args)
}, wait)
} else {
// 第一次执行
// 或者时间间隔超出了设定的时间间隔,执行函数 fn
last = now
func.apply(context, args)
}
}
}
上述代码实现了一个简单的节流函数,不过 underscore 实现了更高级的功能,即新增了两个功能
配置 { leading: false } 时,事件刚开始的那次回调不执行;配置 { trailing: false } 时,事件结束后的那次回调不执行,不过需要注意的是,这两者不能同时配置。
所以在 underscore 中的节流函数有 3 种调用方式,默认的(有头有尾),设置 { leading: false } 的,以及设置 { trailing: false } 的。上面说过实现 throttle 的方案有 2 种,一种是通过时间戳判断,另一种是通过定时器创建和销毁来控制。
第一种方案实现这 3 种调用方式存在一个问题,即事件停止触发时无法响应回调,所以 { trailing: true } 时无法生效。
第二种方案来实现也存在一个问题,因为定时器是延迟执行的,所以事件停止触发时必然会响应回调,所以 { trailing: false } 时无法生效。
underscore 采用的方案是两种方案搭配使用来实现这个功能。
const throttle = function(func, wait, options) {
var timeout, context, args, result;
// 上一次执行回调的时间戳
var previous = 0;
// 无传入参数时,初始化 options 为空对象
if (!options) options = {
};
var later = function() {
// 当设置 { leading: false } 时
// 每次触发回调函数后设置 previous 为 0
// 不然为当前时间
previous = options.leading === false ? 0 : _.now();
// 防止内存泄漏,置为 null 便于后面根据 !timeout 设置新的 timeout
timeout = null;
// 执行函数
result = func.apply(context, args);
if (!timeout) context = args = null;
};
// 每次触发事件回调都执行这个函数
// 函数内判断是否执行 func
// func 才是我们业务层代码想要执行的函数
var throttled = function() {
// 记录当前时间
var now = _.now();
// 第一次执行时(此时 previous 为 0,之后为上一次时间戳)
// 并且设置了 { leading: false }(表示第一次回调不执行)
// 此时设置 previous 为当前值,表示刚执行过,本次就不执行了
if (!previous && options.leading === false) previous = now;
// 距离下次触发 func 还需要等待的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 要么是到了间隔时间了,随即触发方法(remaining <= 0)
// 要么是没有传入 {leading: false},且第一次触发回调,即立即触发
// 此时 previous 为 0,wait - (now - previous) 也满足 <= 0
// 之后便会把 previous 值迅速置为 now
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
// clearTimeout(timeout) 并不会把 timeout 设为 null
// 手动设置,便于后续判断
timeout = null;
}
// 设置 previous 为当前时间
previous = now;
// 执行 func 函数
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 最后一次需要触发的情况
// 如果已经存在一个定时器,则不会进入该 if 分支
// 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支
// 间隔 remaining milliseconds 后触发 later 方法
timeout = setTimeout(later, remaining);
}
return result;
};
// 手动取消
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
// 执行 _.throttle 返回 throttled 函数
return throttled;
};
节流/防抖动画演示网站
木易杨:深入浅出节流/防抖函数
7分钟理解JS的节流、防抖及使用场景
JavaScript专题之跟着 underscore 学节流
Js中的防抖与节流
这是动画演示的代码, 可以通过这个链接下载: 跳转
/**
* Created by thephpjo on 21.04.14.
*/
var helpers = {
/**
* debouncing, executes the function if there was no new event in $wait milliseconds
* @param func
* @param wait
* @param scope
* @returns {Function}
*/
debounce: function(func, wait, scope) {
var timeout
return function() {
var context = scope || this,
args = arguments
var later = function() {
timeout = null
func.apply(context, args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
},
/**
* in case of a "storm of events", this executes once every $threshold
* @param fn
* @param threshhold
* @param scope
* @returns {Function}
*/
throttle: function(fn, threshhold, scope) {
threshhold || (threshhold = 250)
var last, deferTimer
return function() {
var context = scope || this
var now = +new Date(),
args = arguments
if (last && now < last + threshhold) {
// hold on to it
clearTimeout(deferTimer)
deferTimer = setTimeout(function() {
last = now
fn.apply(context, args)
}, threshhold)
} else {
last = now
fn.apply(context, args)
}
}
}
}