JavaScript中如何实现函数节流
在前端开发中,我们经常会遇到一些高频触发的事件,比如页面滚动、窗口 resize、输入框实时搜索等。这些事件如果每次触发都执行对应的处理函数,会大量消耗浏览器性能,甚至导致页面卡顿。函数节流就是一种优化这类场景的技术方案,它的核心思想是:在指定时间内,让函数只执行一次,不管这段时间内事件触发了多少次。
函数节流的核心原理
函数节流的实现逻辑并不复杂,主要有两种常见的实现思路:
- 时间戳版:通过记录上次函数执行的时间,每次触发事件时判断当前时间与上次执行时间的差值,如果大于等于设定的间隔时间,就执行函数并更新上次执行时间。
- 定时器版:触发事件时设置一个定时器,在定时器到期后执行函数,执行完成后清空定时器,直到定时器被清空后才会再次设置新的定时器。
时间戳版节流实现
时间戳版的实现方式会在触发事件时立即执行第一次函数,之后每隔指定间隔执行一次,最后一次触发如果距离上一次执行不足间隔,则不会执行。
/**
* 时间戳版函数节流
* @param {Function} fn 需要节流的目标函数
* @param {number} delay 节流间隔时间,单位毫秒
* @returns {Function} 经过节流处理的函数
*/
function throttleByTimestamp(fn, delay) {
// 记录上次函数执行的时间,初始为0确保第一次触发立即执行
let lastTime = 0;
// 返回包装后的函数
return function(...args) {
// 获取当前时间戳
const currentTime = Date.now();
// 判断当前时间与上次执行时间的差值是否大于等于间隔时间
if (currentTime - lastTime >= delay) {
// 执行目标函数,绑定this上下文,传入参数
fn.apply(this, args);
// 更新上次执行时间为当前时间
lastTime = currentTime;
}
};
}我们可以通过一个滚动事件的例子来验证这个实现,假设需要在页面滚动时打印当前滚动位置,每隔500毫秒最多执行一次:
// 定义需要节流的滚动处理函数
function handleScroll() {
console.log('当前滚动位置:', window.pageYOffset);
}
// 生成节流后的滚动处理函数,间隔500毫秒
const throttledScroll = throttleByTimestamp(handleScroll, 500);
// 监听页面滚动事件
window.addEventListener('scroll', throttledScroll);定时器版节流实现
定时器版的实现方式会在第一次触发事件时等待一个间隔时间再执行函数,之后每次触发如果定时器还在等待中就不执行,直到定时器执行完成并清空,才会重新设置定时器。
/**
* 定时器版函数节流
* @param {Function} fn 需要节流的目标函数
* @param {number} delay 节流间隔时间,单位毫秒
* @returns {Function} 经过节流处理的函数
*/
function throttleByTimer(fn, delay) {
// 定时器标识,初始为null
let timer = null;
// 返回包装后的函数
return function(...args) {
// 如果定时器不存在,才设置新的定时器
if (!timer) {
timer = setTimeout(() => {
// 执行目标函数,绑定this上下文,传入参数
fn.apply(this, args);
// 执行完成后清空定时器标识
timer = null;
}, delay);
}
};
}同样用滚动事件的例子测试,行为和时间戳版略有不同,第一次触发后会等待500毫秒再执行第一次函数:
// 定义需要节流的滚动处理函数
function handleScroll() {
console.log('当前滚动位置:', window.pageYOffset);
}
// 生成节流后的滚动处理函数,间隔500毫秒
const throttledScroll = throttleByTimer(handleScroll, 500);
// 监听页面滚动事件
window.addEventListener('scroll', throttledScroll);两种实现方式的对比
两种实现方式各有特点,我们可以根据实际场景选择:
| 对比维度 | 时间戳版 | 定时器版 |
|---|---|---|
| 首次触发执行时机 | 立即执行 | 等待一个间隔时间后执行 |
| 最后一次触发处理 | 如果距离上次执行不足间隔,不会执行 | 如果定时器还在等待,最后一次触发后会在定时器到期后执行 |
| 适用场景 | 需要首次立即响应,对最后一次触发结果要求不高的场景 | 可以接受首次延迟,需要保证最后一次触发也能得到响应的场景 |
结合两种方式的通用节流函数
如果我们既希望首次触发立即执行,又希望最后一次触发能被处理,可以结合两种方式的优点实现一个通用节流函数:
/**
* 通用版函数节流,结合时间戳和定时器的优点
* @param {Function} fn 需要节流的目标函数
* @param {number} delay 节流间隔时间,单位毫秒
* @returns {Function} 经过节流处理的函数
*/
function throttle(fn, delay) {
let lastTime = 0;
let timer = null;
return function(...args) {
const currentTime = Date.now();
const remainingTime = delay - (currentTime - lastTime);
// 如果距离上次执行已经超过了间隔时间,或者首次执行
if (remainingTime <= 0) {
// 如果有等待中的定时器,先清空
if (timer) {
clearTimeout(timer);
timer = null;
}
// 立即执行函数
fn.apply(this, args);
lastTime = currentTime;
} else if (!timer) {
// 如果还没到执行时间,且没有定时器,就设置一个定时器在剩余时间后执行
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
timer = null;
}, remainingTime);
}
};
}这个通用版本既保留了时间戳版首次立即执行的特性,又通过定时器处理了最后一次触发的情况,适合大多数需要节流的场景。
实际开发中的注意事项
- 节流间隔时间的设置需要结合实际场景,比如滚动场景通常设置100-500毫秒,resize场景设置200-500毫秒比较合适。
- 节流函数返回的是一个新的函数,需要注意绑定正确的this上下文,上面的实现中通过apply已经处理了this的指向问题。
- 如果不再需要节流的函数,记得移除对应的事件监听,避免内存泄漏。
JavaScript函数节流throttle实现时间戳版节流定时器版节流高频事件优化 本作品最后修改时间:2026-05-22 14:02:44