防抖(debouncing)节流(throttling)
TIP
[个人网站] (https://nexuslin.github.io/)
JS
https://nexuslin.github.io/源码地址,求个star
期待你的建议!
JS
GIthub地址:https://gitee.com/lintaibai/TG
Gitee地址:https://gitee.com/lintaibai/TG相遇有缘,特别为你赠诗一首!
JS
同路之人,幸得君顾,盼得君之一赞!
与君同行,愿得青眼相加!
你的star
如春风化雨,润物无声;
如山间清泉,滋润心田;
如长河落日,映照初心;
亦如暗夜明灯,照亮前路;
是吾辈前行之明灯,亦是我坚持的动力!
愿君前程似锦,代码如诗,人生如画!INFO
防抖
事件被触发n秒后再执行回调,n秒内又被触发,则重新计时。(最后一次)
多次触发,只在最后一次触发时,执行函数。
个人理解:防抖的核心是定时器的清除和重新设置。
TIP
节流
在事件n秒内被多次触发,只执行一次。(只一次)
限制目标函数调用的频率,比如:1s内不能调用2次
个人理解:节流的核心是开关
防抖 (debouncing)
WARNING
防抖
- 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
应用场景
- 搜索框输入查询
- 窗口大小resize
- 按钮提交
实际应用
例:用户在搜索框中输入内容,如果用户在3s内继续输入,则重新计时,直到用户停止输入3s后,才会触发搜索事件。
js
function debounce(fn, delay) {
let timer = null;
return function () {
const context = this;
const args = arguments; //保存所有传入参数
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
}
const input = document.getElementById('input');
input.addEventListener('input', debounce(function () {
console.log(this.value);
}, 3000));loadash防抖
js
import _ from 'lodash';
const searchInput = document.getElementById('search');
const resultDiv = document.getElementById('result');
const fetchResults = _.debounce(function(query) {
// 模拟 API 请求
resultDiv.innerText = `Searching for: ${query}`;
}, 300);
searchInput.addEventListener('input', (event) => {
fetchResults(event.target.value);
});实现思路
INFO
- 使用闭包保存定时器
- 返回一个函数,该函数在执行时会清除定时器,并重新设置定时器
- 在定时器到期后执行传入的函数
js
<input type="text" id="search" placeholder="Search...">
<div id="result"></div>
<script>
const searchInput = document.getElementById('search');
const resultDiv = document.getElementById('result');
const fetchResults = debounce(function(query) {
// 模拟 API 请求
resultDiv.innerText = `Searching for: ${query}`;
}, 300);
searchInput.addEventListener('input', (event) => {
fetchResults(event.target.value);
});
</script>防抖优化
防抖函数
JS
// 防抖函数
function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}ES6箭头和剩余参数优化
JS
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};防抖取消执行
作用
INFO
控制执行时机:允许开发者在特定条件下取消即将执行的函数调用,而不是等待防抖时间结束。
资源管理:避免不必要的计算或API调用,节省系统资源。
用户体验优化:在用户操作发生变化时,可以取消之前的操作,只执行最新的操作。
防抖取消
JS
// 防抖函数-优化2-取消执行
const debounce = (func, wait) => {
let timeout;
const debounced = (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
debounced.cancel = () => {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};使用
JS
<button id="cancelBtn">取消执行</button>
// 取消执行
document.getElementById('cancelBtn').addEventListener('click',()=>{
debounce(debounceHandler, 1000).cancel();
const result = document.getElementById('debounceResult');
result.innerHTML = `防抖函数已取消执行`;
log(`防抖函数被取消执行`);
});立即执行immediate
点击上面的防抖函数我们可以感受到,点击以后需要等待几秒才会执行,如果我们希望立即执行,然后再进行防抖,我们可以使用立即执行的防抖函数
JS
/**
* 创建一个防抖函数-优化3-立即执行
* @param {Function} func - 需要防抖的函数
* @param {number} wait - 防抖的时间间隔(毫秒)
* @param {boolean} immediate - 是否在首次触发时立即执行函数
* @returns {Function} - 返回新的防抖函数,带有cancel和pending方法
*/
const debounce = (func, wait, immediate = false) => {
// 存储setTimeout返回的ID
let timeout;
// 标记是否有待执行的函数
let pending = false;
// 创建防抖函数
const debounced = (...args) => {
// 定义延迟执行的函数
const later = () => {
// 清除timeout引用
timeout = null;
// 重置pending状态
pending = false;
// 如果不是立即执行模式,则执行原函数
if (!immediate) func(...args);
};
// 判断是否应该立即执行
// 如果是立即执行模式且当前没有待执行的函数(timeout为null)
const callNow = immediate && !timeout;
// 清除之前的定时器
clearTimeout(timeout);
// 设置新的定时器
timeout = setTimeout(later, wait);
// 标记有待执行的函数
pending = true;
// 如果应该立即执行,则调用函数
if (callNow) func(...args);
};
// 添加取消方法
debounced.cancel = () => {
// 清除定时器
clearTimeout(timeout);
// 清除timeout引用
timeout = null;
// 重置pending状态
pending = false;
};
// 添加检查是否有待执行函数的方法
debounced.pending = () => pending;
// 返回防抖函数
return debounced;
};使用
JS
<button id="immediateBtn">立即执行</button>
// 立即执行
document.getElementById('immediateBtn').addEventListener('click', () => {
debounce(() => console.log('立即执行'), 1000, true)
const result = document.getElementById('debounceResult');
result.innerHTML = `防抖函数立即执行`;
log(`防抖函数立即执行`);
});防抖状态
记录防抖状态,记录上一次执行的时间戳,判断是否需要立即执行
JS
// 创建一个防抖函数-优化4-记录状态
const debounce = (func, wait, immediate = false) => {
// 存储setTimeout返回的ID
let timeout;
// 标记是否有待执行的函数
let pending = false;
// 存储上一次执行的时间戳
let lastExecTime = 0;
// 存储函数执行次数
let executionCount = 0;
// 创建防抖函数
const debounced = (...args) => {
// 记录当前时间
const now = Date.now();
// 定义延迟执行的函数
const later = () => {
// 清除timeout引用
timeout = null;
// 重置pending状态
pending = false;
// 更新执行时间
lastExecTime = Date.now();
// 增加执行计数
executionCount++;
// 如果不是立即执行模式,则执行原函数
if (!immediate) func(...args);
};
// 判断是否应该立即执行
// 如果是立即执行模式且当前没有待执行的函数(timeout为null)
const callNow = immediate && !timeout;
// 清除之前的定时器
clearTimeout(timeout);
// 设置新的定时器
timeout = setTimeout(later, wait);
// 标记有待执行的函数
pending = true;
// 如果应该立即执行,则调用函数
if (callNow) {
func(...args);
// 更新执行时间
lastExecTime = Date.now();
// 增加执行计数
executionCount++;
}
};
// 添加取消方法
debounced.cancel = () => {
// 清除定时器
clearTimeout(timeout);
// 清除timeout引用
timeout = null;
// 重置pending状态
pending = false;
};
// 检查是否有待执行函数的方法
debounced.pending = () => pending;
// 添加获取状态的方法
debounced.getStatus = () => ({
pending, // 是否有待执行的函数
lastExecTime, // 上一次执行的时间戳
executionCount, // 执行次数
wait, // 防抖时间间隔
immediate, // 是否为立即执行模式
timeSinceLastExec: lastExecTime ? Date.now() - lastExecTime : null // 距离上次执行的时间
});
// 返回防抖函数
return debounced;
};使用
JS
<button id="statusBtn">获取状态</button>
// 获取状态
document.getElementById('statusBtn').addEventListener('click', () => {
const debouncedFn = debounce(() => console.log('执行'), 1000, true);
const result = document.getElementById('debounceResult');
const resulttxt = debouncedFn.getStatus();
console.log(resulttxt);
result.innerHTML = `防抖函数状态`;
log(`
防抖函数立即执行
<br/>
pending:${resulttxt.pending}, // 是否有待执行的函数
lastExecTime:${resulttxt.lastExecTime}, // 上一次执行的时间戳
executionCount:${resulttxt.executionCount}, // 执行次数
wait:${resulttxt.wait}, // 防抖时间间隔
immediate:${resulttxt.immediate}, // 是否为立即执行模式
timeSinceLastExec:${resulttxt.timeSinceLastExec} // 距离上次执行的时间
<br/>
`);
});优化调试
JS
/**
* 创建一个防抖函数-优化5
* @param {Function} func - 需要防抖的函数
* @param {number} wait - 防抖的时间间隔(毫秒)
* @param {Object} options - 配置选项
* @param {boolean} options.immediate - 是否在首次触发时立即执行函数
* @param {boolean} options.trailing - 是否在延迟结束后执行函数
* @param {boolean} options.leading - 是否在延迟开始时执行函数
* @returns {Function} - 返回新的防抖函数,带有多个实用方法
*/
const debounce = (func, wait, options = {}) => {
// 合并默认选项
const {
immediate = false,
trailing = true, // 默认在延迟结束后执行
leading = false // 默认不在延迟开始时执行
} = options;
// 存储setTimeout返回的ID
let timeout;
// 标记是否有待执行的函数
let pending = false;
// 存储上一次执行的时间戳
let lastExecTime = 0;
// 存储函数执行次数
let executionCount = 0;
// 存储上一次调用的参数
let lastArgs = null;
// 存储函数名称,用于调试
const funcName = func.name || 'anonymous';
// 创建唯一标识符
const debounceId = `debounce-${funcName}-${Date.now()}`;
// 创建防抖函数
const debounced = (...args) => {
// 记录当前时间
const now = Date.now();
// 保存参数,用于后续执行
lastArgs = args;
// 定义延迟执行的函数
const later = () => {
// 清除timeout引用
timeout = null;
// 重置pending状态
pending = false;
// 如果启用了trailing模式,执行函数
if (trailing && !immediate) {
lastExecTime = Date.now();
executionCount++;
func(...lastArgs);
}
};
// 判断是否应该立即执行
const callNow = (leading || immediate) && !timeout;
// 如果是立即执行模式且不是trailing模式
if (callNow) {
func(...args);
lastExecTime = Date.now();
executionCount++;
}
// 清除之前的定时器
clearTimeout(timeout);
// 如果启用了trailing模式,设置新的定时器
if (trailing) {
timeout = setTimeout(later, wait);
pending = true;
}
};
// 添加取消方法
debounced.cancel = () => {
if (timeout) {
clearTimeout(timeout);
timeout = null;
pending = false;
console.log(`[Debounce] ${debounceId} 已取消`);
}
};
// 添加刷新方法 - 重置计时器但不取消执行
debounced.flush = () => {
if (timeout && pending) {
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
pending = false;
if (trailing && !immediate) {
lastExecTime = Date.now();
executionCount++;
func(...lastArgs);
}
}, wait);
console.log(`[Debounce] ${debounceId} 已刷新计时器`);
}
};
// 添加立即执行方法
debounced.run = () => {
if (pending || !timeout) {
func(...(lastArgs || []));
lastExecTime = Date.now();
executionCount++;
console.log(`[Debounce] ${debounceId} 立即执行`);
}
};
// 检查是否有待执行函数的方法
debounced.pending = () => pending;
// 添加获取状态的方法
debounced.getStatus = () => ({
id: debounceId, // 唯一标识符
pending, // 是否有待执行的函数
lastExecTime, // 上一次执行的时间戳
executionCount, // 执行次数
wait, // 防抖时间间隔
immediate, // 是否为立即执行模式
trailing, // 是否在延迟结束后执行
leading, // 是否在延迟开始时执行
timeSinceLastExec: lastExecTime ? Date.now() - lastExecTime : null, // 距离上次执行的时间
lastArgs: lastArgs ? `[${lastArgs.join(', ')}]` : null // 上一次调用的参数
});
// 添加重置方法 - 重置所有状态
debounced.reset = () => {
debounced.cancel();
lastExecTime = 0;
executionCount = 0;
lastArgs = null;
console.log(`[Debounce] ${debounceId} 已重置`);
};
// 添加调试日志方法
debounced.debug = (message) => {
console.log(`[Debounce] ${debounceId}: ${message}`, debounced.getStatus());
};
// 添加函数标记,便于调试
debounced.displayName = `debounced(${funcName})`;
return debounced;
};使用
JS
// 创建防抖函数
const debouncedFn = debounce((text) => {
console.log(`处理文本: ${text}`);
}, 1000, {
immediate: true,
trailing: true,
leading: false
});
// 调用防抖函数
debouncedFn('Hello');
// 获取状态
console.log(debouncedFn.getStatus());
// 取消执行
debouncedFn.cancel();
// 立即执行
debouncedFn.run();
// 重置状态
debouncedFn.reset();
// 调试
debouncedFn.debug('调试信息');节流(throttling)
DANGER
节流
- 规定在一个单位时间内,只能触发一次函数。如果在这个单位时间内多次触发,只有一次生效。
应用场景
- 滚动加载更多
- 页面缩放
- 鼠标移动
js
function debounce(fn, delay) {
let timer = null;
return function () {
const context = this;
const args = arguments;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
}loadash节流
js
import _ from 'lodash';
const searchInput = document.getElementById('search');
const resultDiv = document.getElementById('result');
const fetchResults = _.throttle(function(query) {
// 模拟 API 请求
resultDiv.innerText = `Searching for: ${query}`;
}, 300);
searchInput.addEventListener('input', (event) => {
fetchResults(event.target.value);
});实际案列和应用
js
<script>
const scrollMessage = document.getElementById('scrollMessage');
// 使用 Lodash 的 throttle 函数
const handleScroll = _.throttle(() => {
const time = new Date().toLocaleTimeString();
scrollMessage.innerText = `Scrolled at: ${time}`;
}, 500); // 每 500 毫秒执行一次
window.addEventListener('scroll', handleScroll);
</script>节流优化
手写节流函数
js
function throttle(func, limit) {
let inThrottle;
return function() {
const context = this;
const args = arguments;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}优化写法
JS
const throttle = (func, limit) => {
let inThrottle;
return (...args) => {
if (!inThrottle) {
inThrottle = true;
func(...args);
setTimeout(() => inThrottle = false, limit);
}
};
};纯HTML案例
Details
手写简单的案例
JS
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手写防抖与节流</title>
<style>
body {
font-family: 'Arial', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
.section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
}
h2 {
color: #333;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
button {
padding: 10px 15px;
font-size: 16px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
transition: background-color 0.3s;
}
button:hover {
background-color: #45a049;
}
.result {
margin-top: 10px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
min-height: 20px;
}
.log {
font-family: monospace;
font-size: 14px;
color: #555;
}
</style>
</head>
<body>
<h1>防抖与节流示例</h1>
<div class="container">
<!-- 防抖示例 -->
<div class="section">
<h2>防抖 (Debounce)</h2>
<p>防抖: 在事件被触发后等待一段时间,如果在这段时间内没有再次触发事件,才执行回调函数。如果在这段时间内再次触发了事件,则重新计时。</p>
<button id="debounceBtn">防抖按钮 (快速点击)</button>
<div class="result" id="debounceResult"></div>
</div>
<!-- 节流示例 -->
<div class="section">
<h2>节流 (Throttle)</h2>
<p>节流: 规定一个时间单位,在这个时间单位内,事件处理函数只能执行一次。如果在这个时间单位内多次触发事件,只有第一次会执行。</p>
<button id="throttleBtn">节流按钮 (快速点击)</button>
<div class="result" id="throttleResult"></div>
</div>
<!-- 日志区域 -->
<div class="section">
<h2>执行日志</h2>
<div class="log" id="logArea"></div>
</div>
</div>
<script>
// 防抖函数
function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}
// 节流函数
function throttle(func, limit) {
let inThrottle;
return function() {
const context = this;
const args = arguments;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 日志函数
function log(message) {
const logArea = document.getElementById('logArea');
const timestamp = new Date().toLocaleTimeString();
logArea.innerHTML += `<div>${timestamp}: ${message}</div>`;
}
// 防抖按钮点击处理
const debounceHandler = function() {
const result = document.getElementById('debounceResult');
const count = parseInt(result.getAttribute('data-count') || 0) + 1;
result.setAttribute('data-count', count);
result.innerHTML = `防抖函数执行次数: ${count}`;
log(`防抖函数执行 - 第${count}次`);
};
// 节流按钮点击处理
const throttleHandler = function() {
const result = document.getElementById('throttleResult');
const count = parseInt(result.getAttribute('data-count') || 0) + 1;
result.setAttribute('data-count', count);
result.innerHTML = `节流函数执行次数: ${count}`;
log(`节流函数执行 - 第${count}次`);
};
// 应用防抖和节流
document.getElementById('debounceBtn').addEventListener('click', debounce(debounceHandler, 1000));
document.getElementById('throttleBtn').addEventListener('click', throttle(throttleHandler, 1000));
</script>
</body>
</html>