防抖与节流
防抖 & 节流
前端开发为防止事件的频繁触发,采用的两种解决方法:防抖 节流
防抖
原理:不管事件触发多少次,都是以最新的一次开始执行
采用定时器 实现设定时间之后才触发 设定时间之内再次触发 会重新计时
function debounce(func, wait) {
var timeout;
return function () {
clearTimeout(timeout)
timeout = setTimeout(func, wait);
}
}
不改变其 this 指向
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context)
}, wait);
}
}
event 对象
JavaScript 在事件处理函数中会提供事件对象event 不使用 debouce 函数,会打印出 MouseEvent 对象,但是用了debouce函数却打印出 undefined
所以修改一下代码
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}
立即执行
我们想要事件触发后函数立即执行,等到停止触发n秒后,才能重新触发执行 加一个 immediate 参数判断是否是立刻执行。
function debounce(func, wait, immediate = false) {
var timeout = null, result;
return function() {
var context = this;
var args = arguments
if (immediate && timeout == null) {
result = func.apply(context, args)
timeout = setTimeout(() => {
timeout = null
}, wait);
return result;
}
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
}
取消
最后我们再思考一个小需求,我希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦,是不是很开心?
function debounce(func, wait, immediate = false) {
var timeout = null, result;
function debounced() {
var context = this;
var args = arguments
if (immediate && timeout == null) {
result = func.apply(context, args)
timeout = setTimeout(() => {
timeout = null
}, wait);
return result;
}
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(context, args)
}, wait);
}
if (immediate) {
debounced.cancel = function () {
clearTimeout(timeout);
timeout = null
}
}
return debounced
}
节流
原理:如果持续触发事件,在预设的时间间隔内,只会执行一次事件,每隔一段时间执行一次。
根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。 我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。
关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
还是用 debounced 那个例子
var container = document.querySelector('#container')
let count = 0
var getUserAction = function (e) {
container.innerText = count++
}
var setUseAction = throttle(getUserAction, 1000);
container.addEventListener('mousemove', setUseAction)
使用时间戳
function throttle(func, wait) {
var context, args;
var previous = 0;
return function () {
/**
* 不用加号是 Thu Sep 01 2022 18:52:49 GMT+0800 (中国标准时间)
* 用加号是 时间戳
*/
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
使用定时器
function throttle(func, wait) {
var timeout = null;
var context, args;
return function () {
context = this;
args = arguments;
if (!timeout) {
func.apply(context, args)
timeout = setTimeout(() => {
timeout = null
}, wait);
}
}
}
以上两种写法能达到的效果是:鼠标移入立刻执行,每隔1秒再执行一次,假设在3.2秒停止鼠标移动,4秒的时候不会有再执行一次,叫做有头无尾
那怎么实现移入一秒后开始执行,在3.2秒停止鼠标移动,4秒的时候再执行一次呢?也就是无头有尾
还是利用定时器,稍作修改
function throttle(func, wait) {
var timeout = null;
var context, args;
return function () {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null
func.apply(context, args)
}, wait);
}
}
}
有头有尾(字节真题)
如果两个都要,既要进入立即执行,又要停止触发时再执行一次,俗称有头有尾
function throttle(func, wait) {
var context, args;
var previous = 0;
var timeout = null;
return function () {
context = this;
args = arguments;
var now = +new Date();
// 计算下次触发 func 函数剩余时间
var remaining = wait - (now - previous)
// 如果剩余时间小于零,就是时间到该执行函数了 || 后面是系统时间被修改的情况
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func.apply(context, args);
previous = now;
}
// 时间没到的话,设置一个定时器,匹配不再触发函数的情况,时间一到就执行
else if (!timeout) {
timeout = setTimeout(() => {
// 更新previous时间,不然会出现func函数连续调用两次
previous = +new Date()
timeout = null
func.apply(context, args);
}, remaining);
}
}
}
参数配置
现在想要有个配置参数,可以让我任选一种模式使用有头有尾、无头有尾、有头无尾
那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:
leading: false 表示禁用第一次执行 trailing: false 表示禁用停止触发的回调
function throttle(func, wait, options) {
var context, args;
var previous = 0;
var timeout = null;
/**
* leading: false 表示禁用第一次执行
* trailing: false 表示禁用停止触发的回调
* 默认有头无尾
*/
var { leading = true, trailing = false } = options || {}
return function () {
context = this;
args = arguments;
/**
* 不用加号是 Thu Sep 01 2022 18:52:49 GMT+0800 (中国标准时间)
* 用加号是 时间戳
*/
var now = +new Date();
// 有头:就0开始remaining会为负立即执行一次;无头:remaining === wait走下面
!previous && (previous = leading ? 0 : now)
// 计算下次触发 func 函数剩余时间
var remaining = wait - (now - previous)
// 如果剩余时间小于零,就是时间到该执行函数了
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func.apply(context, args);
previous = now;
}
// 时间没到的话,设置一个定时器,匹配不再触发函数的情况,时间一到就执行
else if (!timeout && trailing) {
timeout = setTimeout(() => {
// 更新previous时间,不然会出现func函数连续调用两次
previous = leading ? +new Date() : 0
timeout = null
func.apply(context, args);
}, remaining);
}
}
}
易懂版本
/**
*
* @param {function} fn 要节流的函数
* @param {number} wait 需要节流的毫秒
* @param {boolean} leading 首次是否执行
* @param {boolean} trailing 最后一次是否执行
*/
function throttle(fn, wait = 200, leading = true, trailing = false) {
let timer = null
let previous = 0
return function (...args) {
const now = Date.now()
const remaining = wait - (now - previous)
// 首次调用且 leading = false 时,记录时间但不执行
if (leading === false && previous === 0) {
previous = now
}
// 时间间隔已到,立即执行
if (remaining <= 0 || remaining > wait) {
if (timer) {
clearTimeout(timer)
timer = null
}
previous = now
fn.apply(this, args)
}
// 时间间隔未到,且需要 trailing
else if (trailing && !timer) { // ✅ 只在没有 timer 时设置
timer = setTimeout(() => {
previous = leading === false ? 0 : Date.now() // ✅ 正确重置
timer = null
fn.apply(this, args)
}, remaining) // ✅ 使用剩余时间
}
}
}