跳到主要内容

防抖与节流:前端性能优化的必备技巧

阅读需 9 分钟

🚀 在前端开发中,我们经常需要处理频繁触发的事件,如滚动、调整窗口大小、按键操作等。如果不加以控制,这些事件可能导致大量回调函数执行,造成性能问题和糟糕的用户体验。今天,我要介绍两个解决这类问题的实用技巧:防抖(Debounce)节流(Throttle)

🔍 一、什么是防抖和节流

1.1 基本概念

防抖和节流的本质都是优化高频率执行代码的手段。在浏览器中,像 resizescrollkeypressmousemove 等事件可能会连续不断地触发,导致处理函数被频繁调用,消耗大量计算资源,降低页面性能甚至导致卡顿。

简单理解
  • 节流(Throttle): 控制函数执行频率,一段时间内只执行一次
  • 防抖(Debounce): 延迟函数执行,多次触发时只执行最后一次(或第一次)

1.2 形象比喻:电梯的运行策略

想象一下每天上班时大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应:

🏢 电梯策略(假设超时设定为15秒)

  • 节流版电梯:第一个人进来后,电梯15秒后准时运送。不管期间又有多少人进来,都不会影响这个15秒的定时。
  • 防抖版电梯:第一个人进来后,电梯等待15秒。如果期间又有人进来,15秒等待重新计时。直到最后15秒没有新人进来,才开始运送。

🛠️ 二、实现方式

2.1 节流(Throttle)的实现

节流的核心是:在一段时间内,无论触发多少次函数,都只执行一次

2.1.1 时间戳实现方式

使用时间戳实现的节流函数,会在触发事件时立即执行,但后续只有达到间隔时间才会再次执行。

function throttle(fn, delay = 500) {
let lastTime = 0;

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

if (nowTime - lastTime >= delay) {
fn.apply(this, args);
lastTime = nowTime;
}
};
}

特点:首次触发会立即执行,停止触发后无法再次执行

2.1.2 定时器实现方式

使用定时器实现的节流函数,会在延迟后才第一次执行,之后按照间隔执行。

function throttle(fn, delay = 500) {
let timer = null;

return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}

特点:首次触发会延迟执行,停止触发后依然会执行最后一次

2.1.3 结合版(更精确的控制)

结合时间戳和定时器,实现既立即执行,又能确保最后一次触发后还能执行的节流函数。

function throttle(fn, delay = 500) {
let timer = null;
let startTime = Date.now();

return function(...args) {
const currentTime = Date.now();
const remaining = delay - (currentTime - startTime);
const context = this;

clearTimeout(timer);

if (remaining <= 0) {
fn.apply(context, args);
startTime = Date.now();
} else {
timer = setTimeout(() => {
fn.apply(context, args);
startTime = Date.now();
}, remaining);
}
};
}

2.2 防抖(Debounce)的实现

防抖的核心是:延迟执行,若在等待时间内再次触发,则重新计时

2.2.1 基础版本

function debounce(fn, wait = 500) {
let timer = null;

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

clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}

2.2.2 支持立即执行的防抖

有时我们希望第一次触发能立即执行,后续触发才进行防抖处理:

function debounce(fn, wait = 500, 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);
}
};
}

2.3 封装成工具函数

为了方便使用,我们可以将它们封装成工具函数,并添加取消功能:

// 节流工具函数
function throttle(fn, delay = 500) {
let timer = null;
let startTime = Date.now();

const throttled = function(...args) {
const currentTime = Date.now();
const remaining = delay - (currentTime - startTime);
const context = this;

clearTimeout(timer);

if (remaining <= 0) {
fn.apply(context, args);
startTime = Date.now();
} else {
timer = setTimeout(() => {
fn.apply(context, args);
startTime = Date.now();
}, remaining);
}
};

throttled.cancel = function() {
clearTimeout(timer);
timer = null;
startTime = Date.now();
};

return throttled;
}

// 防抖工具函数
function debounce(fn, wait = 500, immediate = false) {
let timer = null;

const debounced = 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);
}
};

debounced.cancel = function() {
clearTimeout(timer);
timer = null;
};

return debounced;
}

🔄 三、区别与选择

3.1 原理对比

特性防抖(Debounce)节流(Throttle)
执行时机等待一段时间后执行按照一定频率执行
触发频率变化时重新计时不影响执行频率
适用场景输入框搜索、窗口调整大小滚动加载、按钮点击
执行次数连续触发只执行最后一次每间隔时间执行一次

3.2 可视化比较

假设在2秒内,我们频繁触发一个事件10次,设置时间间隔为500ms:

  • 节流:会按照500ms的频率执行,约执行4次
  • 防抖:只会在最后一次触发后的500ms执行,只执行1次
图形化理解

假设触发事件的时间点如下所示:

------o------o--o--o-o-o-o--o------o------o---> 时间轴      ↑         ↑        ↑          ↑         ↑      1         2        3          4         5

  • 节流情况下的执行:1 3 5 (固定间隔执行)
  • 防抖情况下的执行:5 (最后一次触发后执行)

🎯 四、应用场景

4.1 防抖(Debounce)应用场景

防抖适用于

只关心最后一次操作,或者需要等待操作完全停止后再执行的场景

  1. 搜索框输入:用户输入完毕后再发送请求,避免频繁请求

    const searchInput = document.getElementById('search');
    const handleSearch = debounce(function(e) {
    console.log('搜索:', e.target.value);
    // API请求
    }, 500);

    searchInput.addEventListener('input', handleSearch);
  2. 窗口大小调整:调整完成后再重新计算布局

    const handleResize = debounce(function() {
    console.log('窗口大小调整完成');
    // 重新计算布局
    }, 300);

    window.addEventListener('resize', handleResize);
  3. 表单验证:用户输入完成后再进行验证

    const phoneInput = document.getElementById('phone');
    const validatePhone = debounce(function(e) {
    const phone = e.target.value;
    const isValid = /^1[3-9]\d{9}$/.test(phone);

    if (!isValid) {
    showError('请输入正确的手机号');
    } else {
    hideError();
    }
    }, 400);

    phoneInput.addEventListener('input', validatePhone);

4.2 节流(Throttle)应用场景

节流适用于

需要持续触发但又不希望频率过高的场景

  1. 滚动加载:滚动时每隔一段时间检查一次是否需要加载更多

    const handleScroll = throttle(function() {
    const { scrollTop, scrollHeight, clientHeight } = document.documentElement;

    if (scrollTop + clientHeight >= scrollHeight - 100) {
    console.log('距离底部100px,加载更多数据');
    loadMoreData();
    }
    }, 300);

    window.addEventListener('scroll', handleScroll);
  2. 按钮点击:防止用户快速多次点击提交按钮

    const submitBtn = document.getElementById('submit');
    const handleSubmit = throttle(function(e) {
    console.log('提交表单');
    // 提交表单逻辑
    }, 1000); // 设置较大的间隔,防止重复提交

    submitBtn.addEventListener('click', handleSubmit);
  3. 游戏中的按键响应:控制角色移动频率

    const handleKeyDown = throttle(function(e) {
    switch(e.key) {
    case 'ArrowUp':
    moveCharacter('up');
    break;
    case 'ArrowDown':
    moveCharacter('down');
    break;
    // 其他按键处理
    }
    }, 100); // 每100ms响应一次按键

    window.addEventListener('keydown', handleKeyDown);

💡 五、实战技巧与最佳实践

5.1 在React中使用

在React组件中使用防抖和节流需要注意函数的引用问题,可以使用useCallback和useEffect来优化:

import React, { useState, useCallback, useEffect } from 'react';

function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);

// 使用useCallback包裹,确保函数引用稳定
const debouncedSearch = useCallback(
debounce(async (searchTerm) => {
if (searchTerm.length < 2) return;

try {
const response = await fetch(`/api/search?q=${searchTerm}`);
const data = await response.json();
setResults(data);
} catch (error) {
console.error('搜索出错:', error);
}
}, 500),
[] // 空依赖数组,确保debounce函数只创建一次
);

// 处理输入变化
const handleInputChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};

// 组件卸载时取消防抖函数
useEffect(() => {
return () => {
debouncedSearch.cancel();
};
}, [debouncedSearch]);

return (
<div>
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="搜索..."
/>
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}

5.2 自定义React Hooks

将防抖和节流封装成自定义hooks,便于在多个组件中复用:

// useDebounce.js
import { useCallback, useEffect, useRef } from 'react';

function useDebounce(fn, delay = 500, deps = []) {
const fnRef = useRef(fn);

// 更新fnRef.current为最新的fn
useEffect(() => {
fnRef.current = fn;
});

const debounced = useCallback(
debounce((...args) => {
fnRef.current(...args);
}, delay),
[delay, ...deps]
);

// 组件卸载时取消防抖
useEffect(() => {
return () => {
debounced.cancel();
};
}, [debounced]);

return debounced;
}

// 使用方式
function SearchComponent() {
const [query, setQuery] = useState('');

const handleSearch = useDebounce((value) => {
// 搜索逻辑
fetchSearchResults(value);
}, 500);

return (
<input
type="text"
onChange={(e) => {
setQuery(e.target.value);
handleSearch(e.target.value);
}}
value={query}
/>
);
}

5.3 性能优化注意事项

注意事项
  1. 选择合适的延迟时间:根据具体场景调整,通常100ms-500ms是合理范围
  2. 考虑移动设备:移动设备上可能需要更长的延迟时间
  3. 清理函数:确保在组件卸载时取消防抖/节流
  4. 避免闭包陷阱:在React中使用useRef保存最新状态

📚 六、总结

防抖和节流是前端性能优化的必备技巧,两者虽然概念相似,但使用场景和效果有明显区别:

  • 防抖(Debounce):适合处理最终状态,等待用户操作完成后再执行
  • 节流(Throttle):适合控制执行频率,按时间间隔执行

选择哪种技术主要取决于你希望函数如何响应:

  • 如果你想等待一系列操作完成后再执行一次,选择防抖
  • 如果你希望在操作过程中按固定频率执行,选择节流

通过合理使用这两种技术,可以显著提高应用性能和用户体验,减少不必要的计算和资源消耗。在实际项目中,防抖和节流常常是解决性能问题的第一道防线。

💡 小提示:为了避免重复编写这些工具函数,可以考虑使用成熟的库,如lodash的_.debounce_.throttle,它们有更完善的实现和边界情况处理。

希望这篇文章对你理解和应用防抖与节流有所帮助!如果有任何问题或建议,欢迎在评论区留言交流。👋

Loading Comments...