JavaScript 性能优化利器:全面解析防抖(Debounce)与节流(Throttle)技术、应用场景及 Lodash、RxJS、vueuse/core Hook 等高效第三方库实践攻略
JavaScript 性能优化利器:全面解析防抖(Debounce)与节流(Throttle)技术、应用场景及 Lodash、RxJS、vueuse/core Hook 等高效第三方库实践攻略
目录
- 前言
- 2.1 防抖(Debounce)概念
- 2.2 节流(Throttle)概念
- 3.1 手写防抖函数
- 3.2 手写节流函数
- 4.1 防抖常见场景
- 4.2 节流常见场景
- 5.1 Lodash 安装与引入
- 5.2 使用
_.debounce
示例 - 5.3 使用
_.throttle
示例 - 5.4 Lodash 参数详解与注意事项
RxJS 中的 DebounceTime 与 ThrottleTime
- 6.1 RxJS 安装与基础概念
- 6.2
debounceTime
用法示例 - 6.3
throttleTime
用法示例 - 6.4 对比与转换:
debounce
vsauditTime
vssampleTime
vueuse/core 中的 useDebounce 与 useThrottle
- 7.1 vueuse 安装与引入
- 7.2
useDebounce
示例 - 7.3
useThrottle
示例 - 7.4 与 Vue 响应式配合实战
- 8.1 原生 vs Lodash vs RxJS vs vueuse
- 8.2 选择建议与组合使用
- 9.1 防抖流程图解
- 9.2 节流流程图解
- 常见误区与调试技巧
- 总结
前言
在开发表现要求较高的 Web 应用时,我们经常会遇到 频繁触发事件 导致性能问题的情况,比如用户持续滚动触发 scroll
、持续输入触发 input
、窗口大小实时变动触发 resize
等。此时,若在回调中做较重的逻辑(如重新渲染、频繁 API 调用),就会造成卡顿、阻塞 UI 或请求过载。防抖(Debounce)与 节流(Throttle)技术为此提供了优雅的解决方案,通过对事件回调做“延迟”“限频”处理,确保在高频率触发时,能以可控的速率执行逻辑,从而极大地优化性能。
本文将从原理出发,向你详细讲解防抖与节流的概念、手写实现、典型应用场景,并系统介绍三类常用高效库的实践:
- Lodash:经典实用,API 简洁;
- RxJS:函数式响应式编程,适用于复杂事件流处理;
- vueuse/core:在 Vue3 环境下的响应式 Hook 工具,集成度高、使用便捷。
通过代码示例、ASCII 图解与应用场景解析,帮助你迅速掌握并灵活运用于生产环境中。
原理与概念解析
2.1 防抖(Debounce)概念
定义:将多次同一函数调用合并为一次,只有在事件触发停止指定时长后,才执行该函数,若在等待期间再次触发,则重新计时。
- 防抖用来 “抖开” 高频触发,只在最后一次触发后执行。
- 典型:搜索输入联想,当用户停止输入 300ms 后再发起请求。
流程图示(最后触发后才执行):
用户输入:——|a|——|a|——|a|——|(停止300ms)|—— 执行 fn()
时间轴(ms):0 100 200 500
- 若在 0ms 输入一次,100ms 又输入,则 0ms 的定时被清除;
- 只有在输入停止 300ms(即比上次输入再过 300ms)后,才调用一次函数。
2.2 节流(Throttle)概念
定义:限制函数在指定时间段内只执行一次,若在等待期间再次触发,则忽略或延迟执行,确保执行频率不超过预设阈值。
- 节流用来 “限制” 高频触发使得函数匀速执行。
- 典型:滚动监听时,若用户持续滚动,确保回调每 100ms 只执行一次。
流程图示(固定步伐执行):
|---100ms---|---100ms---|---100ms---|
触发频率:|a|a|a|a|a|a|a|a|a|a|...
执行时刻:| fn | fn | fn | fn |
- 每隔 100ms,触发一次执行。若在这段时间内多次触发,均被忽略。
手写实现:Vanilla JS 版本
为了理解原理,先手写两个函数。
3.1 手写防抖函数
/**
* debounce(fn, wait, immediate = false)
* @param {Function} fn - 需要防抖包装的函数
* @param {Number} wait - 延迟时长(ms)
* @param {Boolean} immediate - 是否立即执行一次(leading)
* @return {Function} debounced 函数
*/
function debounce(fn, wait, immediate = false) {
let timer = null;
return function (...args) {
const context = this;
if (timer) clearTimeout(timer);
if (immediate) {
const callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait);
if (callNow) fn.apply(context, args);
} else {
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
}
};
}
// 用法示例:
const onResize = debounce(() => {
console.log('窗口大小改变,执行回调');
}, 300);
window.addEventListener('resize', onResize);
timer
:保留上一次定时器引用,若再次触发则清除。immediate
(可选):若为true
,则在第一次触发时立即调用一次,然后在等待期间不再触发;等待期结束后再次触发时会重复上述流程。
3.2 手写节流函数
/**
* throttle(fn, wait, options = { leading: true, trailing: true })
* @param {Function} fn - 需要节流包装的函数
* @param {Number} wait - 最小时间间隔(ms)
* @param {Object} options - { leading, trailing }
* @return {Function} throttled 函数
*/
function throttle(fn, wait, options = {}) {
let timer = null;
let lastArgs, lastThis;
let lastInvokeTime = 0;
const { leading = true, trailing = true } = options;
const invoke = (time) => {
lastInvokeTime = time;
fn.apply(lastThis, lastArgs);
lastThis = lastArgs = null;
};
return function (...args) {
const now = Date.now();
if (!lastInvokeTime && !leading) {
lastInvokeTime = now;
}
const remaining = wait - (now - lastInvokeTime);
lastThis = this;
lastArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timer) {
clearTimeout(timer);
timer = null;
}
invoke(now);
} else if (!timer && trailing) {
timer = setTimeout(() => {
timer = null;
invoke(Date.now());
}, remaining);
}
};
}
// 用法示例:
const onScroll = throttle(() => {
console.log('滚动事件处理,间隔至少 200ms');
}, 200);
window.addEventListener('scroll', onScroll);
lastInvokeTime
:记录上次执行的时间戳,用于计算剩余冷却时间;leading
/trailing
:控制是否在最开始和最后一次触发时各执行一次;- 若触发频繁,则只在间隔结束时执行一次;若在等待期间再次触发符合
trailing
,则在剩余时间后执行。
应用场景剖析
4.1 防抖常见场景
输入搜索联想
const onInput = debounce((e) => { fetch(`/api/search?q=${e.target.value}`).then(/* ... */); }, 300); input.addEventListener('input', onInput);
- 用户停止输入 300ms 后才发请求,避免每个字符都触发请求。
表单校验
const validate = debounce((value) => { // 假设请求服务端校验用户名是否已存在 fetch(`/api/check?username=${value}`).then(/* ... */); }, 500); usernameInput.addEventListener('input', (e) => validate(e.target.value));
窗口大小调整
window.addEventListener('resize', debounce(() => { console.log('重新计算布局'); }, 200));
按钮防重复点击(立即执行模式):
const onClick = debounce(() => { submitForm(); }, 1000, true); // 立即执行,后续 1s 内无效 button.addEventListener('click', onClick);
4.2 节流常见场景
滚动监听
window.addEventListener('scroll', throttle(() => { // 更新下拉加载或固定导航等逻辑 updateHeader(); }, 100));
鼠标移动追踪
document.addEventListener('mousemove', throttle((e) => { console.log(`坐标:${e.clientX}, ${e.clientY}`); }, 50));
动画帧渲染(非
requestAnimationFrame
):window.addEventListener('scroll', throttle(() => { window.requestAnimationFrame(() => { // 渲染 DOM 变化 }); }, 16)); // 接近 60FPS
表单快闪保存(每 2 秒保存一次表单内容):
const onFormChange = throttle((data) => { saveDraft(data); }, 2000); form.addEventListener('input', (e) => onFormChange(getFormData()));
Lodash 中的 Debounce 与 Throttle
5.1 Lodash 安装与引入
npm install lodash --save
# 或者按需加载:
npm install lodash.debounce lodash.throttle --save
在代码中引入:
import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle';
5.2 使用 _.debounce
示例
<template>
<input v-model="query" placeholder="请输入关键字" />
</template>
<script>
import { ref, watch } from 'vue';
import debounce from 'lodash.debounce';
export default {
setup() {
const query = ref('');
// 1. 手动包装一个防抖函数
const fetchData = debounce((val) => {
console.log('发送请求:', val);
// 调用 API
}, 300);
// 2. 监听 query 变化并调用防抖函数
watch(query, (newVal) => {
fetchData(newVal);
});
return { query };
}
};
</script>
Lodash 的
_.debounce
默认为 不立即执行,可传入第三个参数{ leading: true }
使其立即执行一次:const fn = debounce(doSomething, 300, { leading: true, trailing: false });
参数详解:
leading
:是否在开始时立即执行一次;trailing
:是否在延迟结束后再执行一次;maxWait
:指定最长等待时间,防止长时间不触发。
5.3 使用 _.throttle
示例
<template>
<div @scroll="handleScroll" class="scroll-container">
<!-- 滚动内容 -->
</div>
</template>
<script>
import throttle from 'lodash.throttle';
import { onMounted, onUnmounted } from 'vue';
export default {
setup() {
const handleScroll = throttle((event) => {
console.log('滚动位置:', event.target.scrollTop);
}, 100);
onMounted(() => {
const container = document.querySelector('.scroll-container');
container.addEventListener('scroll', handleScroll);
});
onUnmounted(() => {
const container = document.querySelector('.scroll-container');
container.removeEventListener('scroll', handleScroll);
});
return {};
}
};
</script>
- 默认 Lodash 的
_.throttle
会立即执行一次(leading),并在等待结束后执行最后一次(trailing)。 可通过第三个参数控制:
const fn = throttle(fn, 100, { leading: false, trailing: true });
5.4 Lodash 参数详解与注意事项
wait
:至少等待时间,单位毫秒。options.leading
:是否在最前面先执行一次(第一触发立即执行)。options.trailing
:是否在最后面再执行一次(等待期间最后一次触发会在结束时调用)。options.maxWait
(仅限 debounce):最长等待时间,确保在该时间后必定触发一次。
注意:_.debounce
与 _.throttle
返回的都是“可取消”的函数实例,带有 .cancel()
与 .flush()
方法,如:
const debouncedFn = debounce(fn, 300);
// 取消剩余等待
debouncedFn.cancel();
// 立即执行剩余等待
debouncedFn.flush();
RxJS 中的 DebounceTime 与 ThrottleTime
6.1 RxJS 安装与基础概念
RxJS(Reactive Extensions for JavaScript)是一套基于 Observable 可观察流的数据处理库,擅长处理异步事件流。其核心概念:
- Observable:可观察对象,表示一串随时间推移的事件序列。
- Operator:操作符,用于对 Observable 进行转换、过滤、节流、防抖等处理。
- Subscription:订阅,允许你获取 Observable 数据并取消订阅。
安装 RxJS:
npm install rxjs --save
6.2 debounceTime
用法示例
<template>
<input ref="searchInput" placeholder="输入后搜索" />
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';
export default {
setup() {
const searchInput = ref(null);
let subscription;
onMounted(() => {
// 1. 创建可观察流:input 的 keyup 事件
const keyup$ = fromEvent(searchInput.value, 'keyup').pipe(
// 2. 防抖:只在 500ms 内不再触发时发出最后一次值
debounceTime(500),
// 3. 获取输入值
map((event) => event.target.value)
);
// 4. 订阅并处理搜索
subscription = keyup$.subscribe((value) => {
console.log('搜索:', value);
// 调用 API ...
});
});
onUnmounted(() => {
subscription && subscription.unsubscribe();
});
return { searchInput };
}
};
</script>
debounceTime(500)
:表示如果 500ms 内没有新的值到来,则将最后一个值发出;等同于防抖。- RxJS 的
map
、filter
等操作符可组合使用,适用复杂事件流场景。
6.3 throttleTime
用法示例
<template>
<div ref="scrollContainer" class="scrollable">
<!-- 滚动内容 -->
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
import { fromEvent } from 'rxjs';
import { throttleTime, map } from 'rxjs/operators';
export default {
setup() {
const scrollContainer = ref(null);
let subscription;
onMounted(() => {
const scroll$ = fromEvent(scrollContainer.value, 'scroll').pipe(
// 每 200ms 最多发出一次滚动事件
throttleTime(200),
map((event) => event.target.scrollTop)
);
subscription = scroll$.subscribe((pos) => {
console.log('滚动位置:', pos);
// 更新虚拟列表、图表等
});
});
onUnmounted(() => {
subscription && subscription.unsubscribe();
});
return { scrollContainer };
}
};
</script>
<style>
.scrollable {
height: 300px;
overflow-y: auto;
}
</style>
throttleTime(200)
:表示节流,每 200ms 最多发出一个值。- RxJS 中,还有
auditTime
、sampleTime
、debounce
等多种相关操作符,可根据需求灵活选用。
6.4 对比与转换:debounce
vs auditTime
vs sampleTime
操作符 | 特点 | 场景示例 |
---|---|---|
debounceTime | 只在事件停止指定时长后发出最后一次 | 搜索防抖 |
throttleTime | 在指定时间窗口内只发出第一次或最后一次(取决于 config ) | 滚动节流 |
auditTime | 在窗口期结束后发出最新一次值 | 等待窗口结束后再处理(如中断时) |
sampleTime | 定时发出上一次值(即定时取样) | 定时抓取最新状态 |
debounce | 接收一个函数,只有当该函数返回的 Observable 发出值时,才发出源 Observable 上的值 | 复杂场景链式防抖 |
示意图(以每次事件流到来时戳记发射点,|
表示事件到来):
事件流:|---|---|-----|---|----|
debounceTime(200ms): ━━>X (只有最后一个发射)
throttleTime(200ms): |-->|-->|-->|...
auditTime(200ms): |------>|------>|
sampleTime(200ms): |----X----X----X|
vueuse/core 中的 useDebounce 与 useThrottle
7.1 vueuse 安装与引入
vueuse 是一套基于 Vue 3 Composition API 的工具函数集合,包含大量方便的 Hook。
npm install @vueuse/core --save
在组件中引入:
import { ref, watch } from 'vue';
import { useDebounce, useThrottle } from '@vueuse/core';
7.2 useDebounce
示例
<template>
<input v-model="query" placeholder="输入后搜索" />
</template>
<script setup>
import { ref, watch } from 'vue';
import { useDebounce } from '@vueuse/core';
const query = ref('');
// 1. 创建一个防抖的响应式值
const debouncedQuery = useDebounce(query, 500);
// 2. 监听防抖后的值
watch(debouncedQuery, (val) => {
console.log('防抖后搜索:', val);
// 调用 API
});
</script>
useDebounce(source, delay)
:接收一个响应式引用或计算属性,返回一个“防抖后”的响应式引用(ref
)。- 当
query
在 500ms 内不再变化时,debouncedQuery
才更新。
7.3 useThrottle
示例
<template>
<div ref="scrollContainer" class="scrollable">
<!-- 滚动内容 -->
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { useThrottle } from '@vueuse/core';
const scrollTop = ref(0);
const scrollContainer = ref(null);
// 监听原始滚动
scrollContainer.value?.addEventListener('scroll', (e) => {
scrollTop.value = e.target.scrollTop;
});
// 1. 节流后的响应式值
const throttledScroll = useThrottle(scrollTop, 200);
// 2. 监听节流后的值
watch(throttledScroll, (pos) => {
console.log('节流后滚动位置:', pos);
// 更新虚拟列表…
});
</script>
<style>
.scrollable {
height: 300px;
overflow-y: auto;
}
</style>
useThrottle(source, delay)
:将scrollTop
节流后生成throttledScroll
。- 监听
throttledScroll
,确保回调每 200ms 最多执行一次。
7.4 与 Vue 响应式配合实战
<template>
<textarea v-model="text" placeholder="大文本变化时防抖保存"></textarea>
</template>
<script setup>
import { ref, watch } from 'vue';
import { useDebounceFn } from '@vueuse/core'; // 直接防抖函数
const text = ref('');
// 1. 创建一个防抖保存函数
const saveDraft = useDebounceFn(() => {
console.log('保存草稿:', text.value);
// 本地存储或 API 调用
}, 1000);
// 2. 监听 text 每次变化,调用防抖保存
watch(text, () => {
saveDraft();
});
</script>
useDebounceFn(fn, delay)
:创建一个防抖后的函数,与watch
配合使用极其便捷。
性能对比与最佳实践
8.1 原生 vs Lodash vs RxJS vs vueuse
方式 | 代码长度 | 灵活性 | 依赖大小 | 场景适用性 |
---|---|---|---|---|
原生手写 (Vanilla JS) | 最小,需自行管理细节 | 完全可控 | 无依赖 | 简单场景,学习理解时使用 |
Lodash | 代码量少,API 直观 | 灵活可配置 | \~70KB(全量),按需 \~4KB | 绝大多数场景,兼容旧项目 |
RxJS | 需学习 Observable 概念 | 极高,可处理复杂流 | \~200KB(全部) | 复杂异步/事件流处理,如实时图表 |
vueuse/core | 代码极简,集成 Vue | 与 Vue 响应式天然结合 | \~20KB | Vue3 环境下推荐,简化代码量 |
- 原生手写:适合想深入理解原理或无额外依赖需求,但需关注边界情况(如立即执行、取消、节流参数)。
- Lodash:最常用、兼容性好,大多数 Web 项目适用;按需加载可避免打包臃肿。
- RxJS:当事件流之间存在复杂依赖与转换(如“滚动时抛弃前一次节流结果”),RxJS 的组合操作符无可比拟。
- vueuse/core:在 Vue3 项目中,
useDebounce
、useThrottleFn
、useDebounceFn
等 Hook 封装简洁,与响应式系统天然兼容。
8.2 选择建议与组合使用
- 简单场景(如输入防抖、滚动节流):首选 Lodash 或 vueuse。
- 复杂事件流(多种事件链式处理、状态共享):考虑 RxJS。
- Vue3 项目:推荐 vueuse,代码量少、易维护。
- 需支持 IE 或旧项目:用 Lodash(兼容更好)。
图解与数据流示意
9.1 防抖流程图解
事件触发 (User input)
│
├─ 立即清除前一定时器
│
├─ 设置新定时器 (delay = 300ms)
│
└─ 延迟结束后执行回调
↓
fn()
ASCII 图示:
|--t0--|--t1--|--t2--|====(no event for delay)====| fn()
t0, t1, t2
分别为多次触发时间点,中间间隔小于delay
,只有最后一次停止后才会fn()
。
9.2 节流流程图解
事件触发流:|--A--B--C--D--E--F--...
interval = 100ms
执行点: |_____A_____|_____B_____|_____C_____
- 在 A 触发后立即执行(如果
leading: true
),接下来的 B、C、D 触发在 100ms 内均被忽略; - 直到时间窗结束,若
trailing: true
,则执行最后一次触发(如 F)。
时间轴:
0ms: A → fn()
30ms: B (忽略)
60ms: C (忽略)
100ms: (结束此窗) → 若 B/C 中有触发,则 fn() 再执行(取最后一次)
常见误区与调试技巧
“防抖”与“节流”混用场景
- 误区:把防抖当成节流使用,例如滚动事件用防抖会导致滚动结束后才触发回调,体验差。
- 建议:滚动、鼠标移动等持续事件用节流;输入、搜索请求等用防抖。
立即执行陷阱
- Lodash 默认
debounce
不会立即执行,但手写版本有immediate
选项。使用不当会导致业务逻辑在第一次触发时就先行执行。 - 调试技巧:在浏览器控制台加
console.time()
/console.timeEnd()
查看实际调用时机。
- Lodash 默认
定时器未清除导致内存泄漏
- 在组件卸载时,若没有
.cancel()
或clearTimeout()
,定时器仍旧存在,可能误触。 - 建议:在
onBeforeUnmount
生命周期里手动清理。
- 在组件卸载时,若没有
RxJS 组合过度使用
- 误区:遇到一点点防抖需求就引入 RxJS,导致打包过大、学习成本高。
- 建议:只在业务流程复杂(需多操作符组合)时才使用 RxJS,否则 Lodash 或 vueuse 更轻量。
节流丢失最新值
- 若只用
leading: true, trailing: false
,在一段高频触发期间,只有第一触发会执行,后续直到下一窗才可执行,但最终状态可能并非最新。 - 建议:根据业务选择合适的
leading
/trailing
选项。如果要执行最后一次,请设置trailing: true
。
- 若只用
总结
本文从概念原理、手写实现、应用场景,到三大主流库(Lodash、RxJS、vueuse/core)的实践教程,全面拆解了 JavaScript 中的**防抖(Debounce)与节流(Throttle)**技术。核心要点回顾:
- 防抖:将多次高频触发合并,最后一次停止后才执行,适用于搜索输入、校验、按钮防连点等场景。
- 节流:限定函数执行频率,使其在指定时间窗内匀速执行一次,适用于滚动、鼠标移动、窗口大小变化等。
- 手写实现:通过
setTimeout
/clearTimeout
与Date.now()
实现基本防抖与节流逻辑; - Lodash:提供
_.debounce
、_.throttle
,API 简洁易用,可选leading
/trailing
,带有.cancel()
、.flush()
方法; - RxJS:通过
debounceTime
、throttleTime
等操作符,适合复杂事件流处理,需学习 Observable 概念; - vueuse/core:Vue3 专用 Hook,如
useDebounce
、useThrottle
、useDebounceFn
,与响应式系统天然兼容,一行代码解决常见场景; - 最佳实践:根据场景选择最轻量方案,避免过度依赖,注意在组件卸载或业务切换时及时清理定时器/订阅,确保性能与稳定性。
掌握这些技术后,你可以有效避免页面卡顿、请求泛滥,提高前端性能与用户体验,为大型项目的稳定运行保驾护航。希望本文能帮助你系统梳理防抖与节流的方方面面,迅速融会贯通并在实际项目中灵活运用。
评论已关闭