算法 - 节流 (Throttle) 手写实现

1. 题目

实现一个节流函数 throttle(fn, wait):

  • 事件被频繁触发时,每 wait 毫秒最多执行一次 fn。
  • 与防抖不同:节流不要求「安静下来」,而是给执行限速,保证一段时间内至少/至多按节奏跑。
  • 保留正确的 this 与参数。

典型场景:滚动加载、拖拽、游戏连续射击、鼠标 mousemove 计算、大屏 resize。

示例:

1
2
滚动页面每 5ms 触发一次 scroll
throttle(onScroll, 200) -> 每 200ms 才真正执行一次

2. 解题思路

节流有两种主流实现,各自优缺点分明,面试最好都会写。

2.1 时间戳版(保证间隔,可能丢失最后一次)

记录上次执行时间 last。每次触发比较 now - last >= wait:满足就执行并更新 last;否则这次丢弃。

  • 优点:首次触发立即执行(leading)。
  • 缺点:如果最后一次触发落在间隔内,会被丢弃,即「尾巴」不执行(trailing 缺失)。

2.2 定时器版(保证最后一次会执行)

用 timer 作为「是否处于执行中」的锁。触发时若空闲,延迟 wait 后执行并上锁;执行完释放锁。这样停手后仍会把最后一次补执行一次。

  • 优点:保证 trailing 执行。
  • 缺点:首次不是立即执行,而是延迟 wait。

2.3 结合版(leading + trailing)

时间戳 + 定时器合体:既保证首次立即执行,又保证最后一次停止触发后一定收尾。这是 lodash 的行为。

  • 时间复杂度:每次触发 O(1)。

3. TypeScript 实现

3.1 时间戳版

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttleTimestamp<T extends (...args: any[]) => void>(
fn: T,
wait: number
) {
let last = 0;
return function (this: any, ...args: Parameters<T>) {
const now = Date.now();
if (now - last >= wait) {
last = now;
fn.apply(this, args);
}
};
}

3.2 定时器版

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttleTimer<T extends (...args: any[]) => void>(
fn: T,
wait: number
) {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: any, ...args: Parameters<T>) {
if (timer !== null) return; // 执行中,丢弃
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, wait);
};
}

3.3 结合版(leading + trailing,推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function throttle<T extends (...args: any[]) => void>(fn: T, wait: number) {
let timer: ReturnType<typeof setTimeout> | null = null;
let last = 0;

return function (this: any, ...args: Parameters<T>) {
const now = Date.now();
const remaining = wait - (now - last);

if (remaining <= 0) {
// 到点了:立即执行
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
last = now;
fn.apply(this, args);
} else if (timer === null) {
// 没到点且没有排队:挂一个定时器保证最后一次执行
timer = setTimeout(() => {
last = Date.now();
timer = null;
fn.apply(this, args);
}, remaining);
}
};
}

// 用法
const onScroll = throttle(() => console.log("handle scroll"), 200);
window.addEventListener("scroll", onScroll);

4. 面试延伸

  • 节流 vs 防抖 一句话:节流是「匀速放行」,防抖是「攒一波再放」;打字联想用防抖,滚动/射击用节流。
  • requestAnimationFrame 节流:滚动/resize 类视觉更新用 rAF 对齐浏览器帧率比 setTimeout 更流畅省电。
  • passive: true:给 scroll/touch 监听加被动选项,配合节流进一步消除卡顿,全栈性能考点。
  • lodash throttle 本质就是 debounce(fn, wait, { leading: true, trailing: true }) 的特例,理解这层关系能秒杀追问。

难度:中等 | 前端手写题必考 | 性能优化基本功