Vue3 节流函数


Vue3 节流函数

一、前言

在前端开发中,我们经常需要处理一些高频触发的事件,比如滚动事件、鼠标移动事件、窗口大小调整等。如果不对这些事件进行控制,可能会导致页面性能问题,如卡顿、内存泄漏等。节流(Throttle)函数就是一种有效的性能优化技术。

二、什么是节流?

节流(Throttle)是一种限制函数执行频率的技术,它确保一个函数在固定的时间间隔内最多只执行一次。简单来说,就是给函数装上一个”冷却时间”,无论事件触发多么频繁,函数都会按照固定的节奏执行。

节流的核心思想

每隔单位时间内只执行一次,保证在一段时间间隔内一定会执行一次。

节流与防抖的区别

特性防抖 (Debounce)节流 (Throttle)
执行时机事件停止后延迟执行固定时间间隔执行
执行次数只执行最后一次均匀执行多次
响应速度较慢(需要等待事件停止)较快(按固定节奏执行)
内存占用较低(只需要一个定时器)较低(需要保存时间戳或定时器)
适用场景搜索建议、窗口resize、表单验证滚动加载、游戏中的按键处理、实时数据展示
类比电梯门关闭(等人进完)地铁发车(定时出发)
优点避免频繁触发,适合一次性操作保证执行频率,适合连续性操作
缺点可能延迟响应,丢失中间状态可能执行不必要的操作

三、节流函数的实现

1. 定时器版(延迟执行)

这是最简单的节流实现方式,使用定时器来控制函数的执行频率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 节流函数 - 控制函数执行频率,只执行最后一次调用
* @param {Function} func - 需要节流的函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {Function} 节流后的函数
*/
const throttle = (func, delay) => {
let timeoutId; // 闭包保存的变量
return function (...args) {
// 清除之前的定时器,只执行最后一次调用
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
timeoutId = null;
}, delay);
};
};

闭包的作用

  1. 保存状态timeoutId 变量被闭包保存,使得每次调用节流函数时都能访问到同一个变量。
  2. 维持上下文:返回的函数能够访问外部函数的变量(timeoutId),即使外部函数已经执行完毕。
  3. 私有化变量timeoutId 对外部不可见,避免了全局变量污染。

闭包工作原理

  • throttle 函数执行时,它创建了一个作用域,其中包含 timeoutId 变量。
  • throttle 返回一个新函数时,这个新函数会形成一个闭包,包含了对 timeoutId 变量的引用。
  • 即使 throttle 函数执行完毕,其作用域不会被垃圾回收,因为返回的函数仍然引用着这个作用域中的变量。
  • 当返回的函数被调用时,它可以访问和修改 timeoutId 变量,实现了状态的持久化。

特点

  • 事件触发时不会立即执行,而是等待间隔时间后执行
  • 如果间隔内再次触发,会重置定时器(最终只执行最后一次)
  • 保证最后一次触发一定会执行

2. 时间戳版(立即执行)

使用时间戳来控制函数的执行频率,首次触发时立即执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 节流函数(时间戳版本):在 wait 时间内最多执行一次
* @param {Function} func - 要节流的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} - 包装后的节流函数
*/
function throttle(func, wait) {
let previous = 0; // 上一次执行的时间戳

return function (...args) {
const context = this;
const now = Date.now();

if (now - previous >= wait) {
func.apply(context, args);
previous = now;
}
};
}

特点

  • 事件触发时立即执行一次,之后在间隔内不重复执行
  • 间隔结束后才会再次响应
  • 执行时机确定,但末次触发可能被忽略

3. 综合版(立即+延迟)

结合两种方式的优点,首次触发立即执行,间隔内再次触发会在间隔结束后补充执行一次。

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
31
32
33
34
35
/**
* 节流函数(最终版):首次立即执行,末次也保证执行
* @param {Function} func - 要节流的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} - 包装后的节流函数
*/
function throttle(func, wait) {
let previous = 0;
let timeout = null;

const later = function (...args) {
previous = Date.now();
timeout = null;
func.apply(this, args);
};

return function (...args) {
const context = this;
const now = Date.now();
const remaining = wait - (now - previous);

if (remaining <= 0 || remaining > wait) {
// 如果可以执行,清除定时器,立即执行
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
// 否则,设置定时器,保证末次执行
timeout = setTimeout(later.bind(context, ...args), remaining);
}
};
}

特点

  • 首次触发立即执行
  • 间隔内再次触发会在间隔结束后补充执行一次
  • 避免最后一次触发被忽略

4. 三种实现方式的对比

实现方式立即执行末次执行保证内存占用执行时机适用场景
定时器版低(1个定时器)延迟执行输入框搜索、表单提交
时间戳版极低(仅时间戳)立即执行滚动位置计算、实时数据展示
综合版低(1个定时器+时间戳)立即+延迟窗口resize、复杂计算

四、节流函数的应用场景

1. 页面滚动事件

监听滚动位置,实现吸顶导航栏或滚动加载更多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
window.addEventListener(
"scroll",
throttle(function () {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
console.log("滚动位置:", scrollTop);

// 滚动加载更多
if (
window.innerHeight + window.scrollY >=
document.body.offsetHeight - 500
) {
loadMoreItems();
}

// 实现吸顶导航栏
const header = document.querySelector("header");
if (scrollTop > 100) {
header.classList.add("sticky");
} else {
header.classList.remove("sticky");
}
}, 200),
);

最佳实践:对于滚动事件,建议使用时间戳版节流,因为用户需要实时看到滚动效果。

2. 鼠标移动事件

绘制轨迹或更新提示框位置。

1
2
3
4
5
6
element.addEventListener(
"mousemove",
throttle(function (e) {
updateTooltipPosition(e.clientX, e.clientY);
}, 50),
);

最佳实践:鼠标移动事件频率很高,建议使用50-100ms的节流时间,平衡性能和响应速度。

3. 窗口大小调整

响应式布局调整。

1
2
3
4
5
6
window.addEventListener(
"resize",
throttle(function () {
updateLayout();
}, 250),
);

最佳实践:窗口调整事件建议使用综合版节流,既保证立即响应,又确保最后一次调整能够正确处理。

4. 游戏按键处理

防止按键重复触发。

1
2
3
4
5
6
7
8
document.addEventListener(
"keydown",
throttle(function (e) {
if (e.key === "ArrowRight") {
player.moveRight();
}
}, 100),
);

最佳实践:游戏中的按键处理建议使用时间戳版节流,确保操作的即时响应。

5. 实时数据展示

如股票行情、实时监控等。

1
2
3
4
5
6
7
8
9
10
11
12
13
function updateRealTimeData() {
fetch("/api/realtime-data")
.then((response) => response.json())
.then((data) => {
updateDashboard(data);
});
}

// 每500ms更新一次
const throttledUpdate = throttle(updateRealTimeData, 500);

// 启动实时更新
setInterval(throttledUpdate, 100);

6. 拖拽操作

处理拖拽过程中的位置更新。

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
31
32
let isDragging = false;
let startX, startY;

const element = document.querySelector(".draggable");

element.addEventListener("mousedown", function (e) {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
});

document.addEventListener(
"mousemove",
throttle(function (e) {
if (!isDragging) return;

const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;

// 更新元素位置
const rect = element.getBoundingClientRect();
element.style.left = rect.left + deltaX + "px";
element.style.top = rect.top + deltaY + "px";

startX = e.clientX;
startY = e.clientY;
}, 16),
); // 约60fps

document.addEventListener("mouseup", function () {
isDragging = false;
});

最佳实践:拖拽操作建议使用16ms左右的节流时间,接近屏幕刷新率,保证平滑的视觉效果。

五、在Vue3中的使用

1. 基础使用

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
31
32
33
34
35
<template>
<div>
<button @click="handleClick">点击我</button>
<div @mousemove="handleMouseMove">鼠标移动区域</div>
</div>
</template>

<script setup>
import { ref } from "vue";

const count = ref(0);

// 节流函数
const throttle = (func, delay) => {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
timeoutId = null;
}, delay);
};
};

// 节流处理点击事件
const handleClick = throttle(() => {
console.log("按钮被点击");
count.value++;
}, 1000);

// 节流处理鼠标移动
const handleMouseMove = throttle((e) => {
console.log("鼠标位置:", e.clientX, e.clientY);
}, 100);
</script>

2. 使用Composition API封装

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<template>
<div>
<button @click="throttledClick">点击我</button>
<div
ref="scrollContainer"
@scroll="throttledScroll"
style="height: 200px; overflow: auto;"
>
<div style="height: 1000px;">滚动内容</div>
</div>
</div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from "vue";

// 封装节流Hook
function useThrottle(fn, delay = 300) {
let timeoutId = null;

const throttledFn = (...args) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
fn.apply(this, args);
timeoutId = null;
}, delay);
};

// 清理函数
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
};

return { throttledFn, cleanup };
}

// 使用示例
const count = ref(0);

const handleClick = () => {
console.log("按钮被点击");
count.value++;
};

const { throttledFn: throttledClick, cleanup: cleanupClick } = useThrottle(
handleClick,
1000,
);

const handleScroll = () => {
console.log("滚动事件触发");
};

const { throttledFn: throttledScroll, cleanup: cleanupScroll } = useThrottle(
handleScroll,
200,
);

// 组件卸载时清理
onUnmounted(() => {
cleanupClick();
cleanupScroll();
});
</script>

3. 使用第三方库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<button @click="throttledClick">点击我</button>
</div>
</template>

<script setup>
import { ref } from "vue";
import { throttle } from "lodash-es";

const count = ref(0);

const handleClick = () => {
console.log("按钮被点击");
count.value++;
};

// 使用lodash的throttle
const throttledClick = throttle(handleClick, 1000);
</script>

4. 高级使用:自定义Hook扩展

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<template>
<div>
<button @click="increment">点击增加</button>
<p>计数: {{ count }}</p>
<div @mousemove="onMouseMove">鼠标移动区域</div>
<p>鼠标位置: {{ mousePosition.x }}, {{ mousePosition.y }}</p>
</div>
</template>

<script setup>
import { ref } from "vue";

// 高级节流Hook
function useThrottle(fn, delay = 300, options = {}) {
const { leading = false, trailing = true } = options;
let timeoutId = null;
let lastCallTime = 0;

const throttledFn = (...args) => {
const now = Date.now();

// 首次调用且允许立即执行
if (!lastCallTime && !leading) {
lastCallTime = now;
}

const remaining = delay - (now - lastCallTime);

if (remaining <= 0 || remaining > delay) {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastCallTime = now;
fn.apply(this, args);
} else if (!timeoutId && trailing) {
timeoutId = setTimeout(() => {
lastCallTime = leading ? Date.now() : 0;
timeoutId = null;
fn.apply(this, args);
}, remaining);
}
};

const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
lastCallTime = 0;
};

return { throttledFn, cleanup };
}

// 使用示例
const count = ref(0);
const mousePosition = ref({ x: 0, y: 0 });

const increment = () => {
count.value++;
};

const { throttledFn: throttledIncrement } = useThrottle(increment, 1000, {
leading: true, // 立即执行
trailing: false, // 不延迟执行
});

const updateMousePosition = (e) => {
mousePosition.value = { x: e.clientX, y: e.clientY };
};

const { throttledFn: onMouseMove } = useThrottle(updateMousePosition, 50);
</script>

六、注意事项

1. this指向问题

使用apply(this, args)确保函数执行时的this指向正确。在箭头函数中,this会继承外部作用域的this,所以不需要特别处理。

1
2
3
4
5
6
7
8
9
// 正确处理this指向
const throttledFn = throttle(function () {
console.log(this); // 指向调用者
}, 1000);

// 箭头函数的this处理
const throttledFn2 = throttle(() => {
console.log(this); // 指向外部作用域的this
}, 1000);

2. 参数传递

通过...args接收并传递事件参数(如event对象),确保原函数能够接收到完整的参数信息。

1
2
3
4
5
6
7
const handleClick = throttle((event, data) => {
console.log(event.target);
console.log(data);
}, 1000);

// 调用时传递参数
element.addEventListener("click", (e) => handleClick(e, { id: 1 }));

3. 内存泄漏

在组件卸载时清理定时器,避免内存泄漏。特别是在单页应用中,组件频繁挂载和卸载时更要注意。

1
2
3
4
5
6
onUnmounted(() => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
});

4. 场景选择

  • 时间戳版:适合需要立即响应的场景(如实时计算滚动位置)
  • 定时器版:适合需要延迟处理的场景(如输入框搜索联想)
  • 综合版:适合需要兼顾首次立即执行和最后一次延迟执行的场景(如窗口resize后调整布局)

5. 节流时间的选择

  • 高频事件(如鼠标移动、滚动):50-100ms
  • 中频事件(如窗口resize、表单输入):200-300ms
  • 低频事件(如按钮点击):1000ms左右
  • 拖拽操作:16ms(约60fps)

6. 错误处理

在节流函数中添加错误处理,确保原函数的错误不会影响节流逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const throttle = (func, delay) => {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
try {
func.apply(this, args);
} catch (error) {
console.error("Throttled function error:", error);
}
timeoutId = null;
}, delay);
};
};

文章作者: 栖桐听雨声
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 栖桐听雨声 !
  目录