欢迎光临
我们一直在努力

前端老铁别硬扛:手写防抖节流太累,2026年主流库实战避坑指南

在这里插入图片描述

前端老铁别硬扛:手写防抖节流太累,2026年主流库实战避坑指南

  • 前端老铁别硬扛:手写防抖节流太累,2026年主流库实战避坑指南
    • 开头先唠两句
    • 这俩兄弟到底是个啥鬼
    • 现在的库都卷成啥样了
      • Lodash:老大哥还是稳
      • Underscore:廉颇老矣
      • RxJS:函数式编程的"重炮"
      • 轻量级选手:just-debounce-it 和 throttle-debounce
      • WASM 狠人:rust-debounce 和 friends
    • 选错了真的会谢
      • 坑一:定时器清理不干净,内存泄漏到怀疑人生
      • 坑二:异步地狱,Promise 状态乱套
      • 坑三:this 指向迷之丢失
      • 坑四:时间参数的动态调整
    • 真实项目里怎么骚操作
      • 搜索框的终极方案:防抖 + 请求取消 + 竞态处理
      • 无限滚动加载:节流的参数调优艺术
      • 拖拽排序:防抖节流的组合拳
      • 窗口 Resize:性能杀手怎么治
    • 遇到 Bug 别只会重启
      • 调试技巧一:Devtools Performance 面板
      • 调试技巧二:检查多个实例
      • 调试技巧三:内存泄漏检测
    • 几个让同事喊爸爸的 trick
      • Trick 1:通用 Hook 封装,一行代码搞定
      • Trick 2:柯里化预设配置
      • Trick 3:网络自适应延迟
      • Trick 4:组合键防抖
    • 最后碎碎念

前端老铁别硬扛:手写防抖节流太累,2026年主流库实战避坑指南

开头先唠两句

咱就是说,现在谁还自己在那吭哧吭哧手写 debounce 和 throttle 啊?上次我手贱写了个,结果在群里被大佬喷成筛子,说逻辑有漏洞,高并发下直接原地爆炸。今天咱不整那些虚头八脑的理论,就聊聊怎么挑个靠谱的库,把搜索框、滚动加载这些让人头秃的场景给拿捏了,顺便吐槽一下那些踩过的坑,保你看完就能去项目里"抄作业"。

说实话,防抖和节流这俩概念,前端面试必问,简历上必写"精通",但真到项目里用的时候,十个有八个都在瞎搞。我见过最离谱的代码,是把防抖函数写在组件的 render 里,每次更新都重新定义,那防抖个寂寞啊?还有更绝的,在 Vue 的 computed 里用 throttle,结果响应式一触发,定时器直接乱套,页面卡得跟 PPT 似的。

所以啊,与其自己造轮子造得稀烂,不如找个靠谱的库。但问题来了,2026 年了,这俩函数的库早就卷成麻花了,从老牌 lodash 到各种新兴工具,从纯 JS 到 WASM 实现,选哪个?怎么用?坑在哪?今天咱就掰开了揉碎了聊。

这俩兄弟到底是个啥鬼

别被名字唬住了,其实道理特简单。防抖(debounce)就是你疯狂点按钮,它等你消停了再执行,像极了等女朋友化完妆出门;节流(throttle)就是不管你点多快,它只按固定节奏来,像极了地铁进站,到点才开门。

但这里有个误区,很多人以为防抖就是延迟执行,节流就是固定间隔。其实防抖还有"立即执行"模式,比如你点搜索按钮,第一次立即搜,后面狂点不管,等你不点了再补一次。这种模式在实战中特别实用,但自己手写很容易漏掉边界情况。

// 这是我自己曾经写过的"自信满满"版本,后来被大佬喷得体无完肤
function myDebounce(fn, delay) {
let timer = null;
return function(args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args); // 这里其实有问题,this 指向可能丢失
}, delay);
};
}

// 问题在哪?
// 1. 没有立即执行选项
// 2. 没有取消功能
// 3. 没有返回值处理
// 4. 高并发下 timer 状态可能混乱
// 5. 没考虑 leading 和 trailing 的组合场景

看到没?就这几行代码,坑多得能埋人。这还是最简单的防抖,要是加上节流的 leading、trailing 控制,还有 requestAnimationFrame 的优化版本,代码量直接翻倍,bug 也翻倍。所以啊,专业的事交给专业的库,咱们把精力放在业务逻辑上不好吗?

现在的库都卷成啥样了

市面上那几个头部库,像 lodash 这种老牌劲旅,虽然稳但有点重;还有那种专门搞函数工具的小众库,轻是轻,但文档写得跟天书似的。最近还冒出来几个基于 Rust 编译到 WASM 的狠角色,性能炸裂,但兼容性又让人心里打鼓。

Lodash:老大哥还是稳

先说 lodash,这玩意儿在前端圈混了十几年了,debounce 和 throttle 是它的看家本领。优点是稳如老狗,文档齐全,TypeScript 支持完美。缺点是体积大,如果你只用这俩函数,打包进去 70 多 KB(虽然可以按需引入,但配置麻烦)。

// lodash 的用法,经典但有点繁琐
import { debounce, throttle } from 'lodash';

// 防抖 – 等用户输完 300ms 再搜
const searchDebounce = debounce((keyword) => {
console.log('搜索:', keyword);
return fetch(`/api/search?q=${keyword}`); // 支持返回 Promise
}, 300, {
leading: false, // 首次不立即执行
trailing: true, // 停止后执行最后一次
maxWait: 1000 // 最长等待 1 秒,防止一直输入永远不执行
});

// 节流 – 滚动事件每 100ms 最多触发一次
const scrollThrottle = throttle(() => {
console.log('滚动位置:', window.scrollY);
updateLazyImages(); // 懒加载图片
}, 100, {
leading: true, // 首次立即执行
trailing: false // 停止后不补执行
});

// 高级玩法:取消和刷新
searchDebounce('前端');
searchDebounce.cancel(); // 突然不想搜了,直接取消

// 还有 flush,立即执行并清空队列
scrollThrottle.flush();

看到没?lodash 的 options 配置丰富到变态,leading、trailing、maxWait 这三个参数组合起来,能覆盖 99% 的业务场景。特别是 maxWait,很多人不知道这个参数干嘛的。举个例子,你在一个长文本输入框里打字,如果用户一直不停,普通的防抖可能永远得不到执行机会,maxWait 就是兜底策略,强制最多等 1 秒必须执行一次。

但 lodash 也有让人吐槽的地方。它的 debounce 返回的函数,this 指向是绑死的,如果你在 React 类组件里用,经常需要 .bind(this) 或者箭头函数包一层,不然 this 指飞了你都找不到北。

Underscore:廉颇老矣

Underscore 算是 lodash 的前辈,现在用的人少了,但一些老项目还在用。它的 API 设计和 lodash 很像,但功能少很多,比如没有 maxWait,没有 flush。如果你还在维护十年前的项目,可能会遇到它,但新项目不建议用了,毕竟 lodash 几乎完全兼容它,还更强。

RxJS:函数式编程的"重炮"

RxJS 这玩意儿,学的时候觉得脑子不够用,用的时候觉得真香。它把防抖节流当成流的操作符来处理,概念上更统一,但学习曲线陡得能攀岩。

import { fromEvent } from 'rxjs';
import { debounceTime, throttleTime, distinctUntilChanged, switchMap } from 'rxjs/operators';

// 搜索框防抖 + 取消上一次请求
const searchInput = document.getElementById('search');

fromEvent(searchInput, 'input')
.pipe(
debounceTime(300), // 防抖 300ms
distinctUntilChanged(), // 值没变就不触发(比如按方向键)
switchMap(event => { // 自动取消上一次的 Observable
const keyword = event.target.value;
console.log('真正发请求:', keyword);
return fetch(`/api/search?q=${keyword}`).then(r => r.json());
})
)
.subscribe(results => {
renderSearchResults(results);
});

// 滚动节流,带 leading 和 trailing 控制
fromEvent(window, 'scroll')
.pipe(
throttleTime(100, undefined, {
leading: true, // 开始立即执行
trailing: true // 结束也执行一次
})
)
.subscribe(() => {
checkScrollPosition();
});

RxJS 的好处是,防抖节流只是它庞大工具箱里的两个小螺丝刀,配合 switchMap、concatMap 这些操作符,能完美解决"旧请求覆盖新数据"这种经典痛点。坏处是,为了用个防抖,你得引入整个 RxJS,哪怕 tree-shaking 也得几十 KB,小项目有点杀鸡用牛刀。

轻量级选手:just-debounce-it 和 throttle-debounce

如果你就是嫌 lodash 太重,可以考虑这些专门做一件事的库。just-debounce-it 只有 200 多字节,throttle-debounce 稍微胖点,但 API 设计得很现代。

// just-debounce-it,极简主义者的最爱
import debounce from 'just-debounce-it';

const myEfficientDebounce = debounce((data) => {
console.log('处理数据:', data);
}, 250, true); // 第三个参数是 immediate,立即执行

// 取消也很简单
myEfficientDebounce('test');
myEfficientDebounce.cancel();

// throttle-debounce,支持 async/await 更友好
import { debounce as smartDebounce, throttle as smartThrottle } from 'throttle-debounce';

// 这个库的 debounce 支持 Promise 返回,还能拿到执行结果
const asyncDebounce = smartDebounce(500, async (id) => {
const res = await fetch(`/api/user/${id}`);
return res.json();
});

// 调用时可以 await
const userData = await asyncDebounce(123);
console.log('用户信息:', userData);

这些库的优点是体积小,缺点是功能相对单一。比如 just-debounce-it 就没有 maxWait,throttle-debounce 的 leading/trailing 控制不如 lodash 灵活。适合对包大小敏感,且需求简单的场景。

WASM 狠人:rust-debounce 和 friends

最近逛 GitHub 发现几个用 Rust 写的防抖节流库,编译成 WASM 跑在浏览器里。理论上性能应该炸裂,毕竟 Rust 没有 GC,内存管理更精细。但实际测下来,在普通业务场景里,和 JS 版本差距不大,只有在极端高频触发(比如鼠标移动事件每秒上千次)时才能看出优势。

// 假设你用了一个 WASM 版本的防抖库(伪代码,具体 API 看具体库)
import init, { create_debounce } from 'rust-debounce';

await init(); // 初始化 WASM 模块

const wasmDebounce = create_debounce(
(x, y) => console.log('鼠标位置:', x, y),
16 // 约等于 60fps
);

// 在 mousemove 里用,理论上性能更好
document.addEventListener('mousemove', (e) => {
wasmDebounce(e.clientX, e.clientY);
});

这种库的坑在于,WASM 的启动有异步初始化过程,而且和 JS 的交互有序列化开销。如果你的防抖函数很简单,WASM 的调用成本可能比节省的 CPU 时间还高。另外,调试困难,报错了堆栈信息全是 wasm 代码,看得你怀疑人生。建议除非你在做图形编辑器、游戏这种高频交互应用,否则别折腾。

选错了真的会谢

有些库看着挺美,一上生产环境就露馅。比如有的在处理快速连续触发时,最后一次执行会丢数据;有的在定时器清理上不干净,内存泄漏让你页面越跑越卡,最后浏览器直接教你做人。还有的对 TypeScript 支持极差,类型推断全红,逼得你只能 any 走天下,这谁能忍?

坑一:定时器清理不干净,内存泄漏到怀疑人生

这是最隐蔽的坑。很多库的 debounce 内部用 setTimeout,但如果你没正确取消,组件卸载了定时器还在跑,轻则内存泄漏,重则回调里访问了已经销毁的 DOM,直接报错。

// React 组件里的错误示范
function SearchComponent() {
const [results, setResults] = useState([]);

// 错误!每次渲染都创建新的 debounce 函数
const handleSearch = debounce((keyword) => {
fetchResults(keyword).then(setResults);
}, 300);

return <input onChange={(e) => handleSearch(e.target.value)} />;
}

// 正确做法:用 useMemo 或 useCallback 缓存
function SearchComponentFixed() {
const [results, setResults] = useState([]);

// 用 useMemo 保证只创建一次
const handleSearch = useMemo(
() => debounce((keyword) => {
fetchResults(keyword).then(setResults);
}, 300),
[] // 空依赖,只初始化一次
);

// 组件卸载时取消
useEffect(() => {
return () => {
handleSearch.cancel(); // 清理 pending 的定时器
};
}, [handleSearch]);

return <input onChange={(e) => handleSearch(e.target.value)} />;
}

看到没?React 里用防抖,必须用 useMemo 或者 useRef 来保持函数引用稳定,不然每次渲染都是新的函数,防抖个寂寞。而且卸载时一定要 cancel,不然你在组件 A 里发的请求,回调里 setState,结果组件 A 已经卸载了,React 会报警告,严重点整个应用崩溃。

坑二:异步地狱,Promise 状态乱套

如果你防抖的是一个 async 函数,要特别注意执行顺序。有些库的 debounce 不会等你 Promise 完成,只是延迟调用。如果连续触发,可能会同时存在多个 pending 的 Promise,最后哪个先回来还真不好说。

// 错误示范:自己瞎封装的 async 防抖
function badAsyncDebounce(fn, delay) {
let timer;
return function(args) {
clearTimeout(timer);
timer = setTimeout(async () => { // async 回调,但 debounce 不管 Promise
await fn.apply(this, args);
}, delay);
};
}

// 问题:如果 fn 执行时间很长,期间又触发了,不会取消上一次的执行
// 只是延迟了下一次的调用时间,两次请求可能同时存在

// 正确做法:用 AbortController 取消请求,或者用能处理 Promise 的库
import { debounce } from 'throttle-debounce';

function SearchComponentPro() {
const abortControllerRef = useRef(null);

const searchDebounce = useMemo(
() => debounce(300, async (keyword) => {
// 取消上一次的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}

// 创建新的 controller
abortControllerRef.current = new AbortController();

try {
const res = await fetch(`/api/search?q=${keyword}`, {
signal: abortControllerRef.current.signal
});
const data = await res.json();
setResults(data);
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求被取消,这是正常的');
} else {
console.error('搜索出错:', err);
}
}
}),
[]
);

useEffect(() => {
return () => {
searchDebounce.cancel();
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [searchDebounce]);

return <input onChange={(e) => searchDebounce(e.target.value)} />;
}

这个例子结合了防抖和请求取消,是搜索框的终极解决方案。AbortController 是现代浏览器提供的标准 API,能真正取消 fetch 请求,而不是仅仅忽略结果。配合 debounce,既避免了频繁请求,又保证了数据一致性。

坑三:this 指向迷之丢失

这是 JS 的老问题了,但在防抖场景下特别容易踩。因为 debounce 返回的是新函数,原函数的 this 上下文会丢失。

class SearchManager {
constructor() {
this.cache = new Map();
// 错误!这里 this 会指向 window 或 undefined(严格模式)
this.debouncedSearch = debounce(this.search, 300);
}

search(keyword) {
// 这里的 this 是 undefined!
console.log(this.cache); // 报错!
}
}

// 解决方案 1:箭头函数
class SearchManagerFixed {
constructor() {
this.cache = new Map();
this.debouncedSearch = debounce((keyword) => this.search(keyword), 300);
}

search(keyword) {
console.log(this.cache); // 正常
}
}

// 解决方案 2:bind
class SearchManagerBind {
constructor() {
this.cache = new Map();
this.debouncedSearch = debounce(this.search.bind(this), 300);
}
}

// 解决方案 3:用 class fields 语法(最优雅)
class SearchManagerModern {
cache = new Map();

// 箭头函数属性,自动绑定 this
debouncedSearch = debounce((keyword) => {
console.log(this.cache); // 完美
return this.fetchData(keyword);
}, 300);

async fetchData(keyword) {
// …
}
}

如果你用 TypeScript,第三个方案最爽,类型推断完美,this 也不会丢。但注意,class fields 语法创建的 debounce 函数是每个实例独立的,如果你创建 1000 个实例,就有 1000 个 debounce 函数和定时器,内存占用要考虑。

坑四:时间参数的动态调整

有些场景需要动态调整防抖延迟,比如网络好的时候 300ms,网络差的时候 100ms。但大多数库的 delay 参数是固定的,创建后不能改。

// 自己封装一个支持动态延迟的防抖
function createAdaptiveDebounce(fn) {
let timer = null;
let currentDelay = 300; // 默认 300ms

const debounced = function(args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, currentDelay);
};

// 暴露修改延迟的方法
debounced.setDelay = (newDelay) => {
currentDelay = newDelay;
};

debounced.cancel = () => {
clearTimeout(timer);
};

return debounced;
}

// 使用:根据网络状况调整
const adaptiveSearch = createAdaptiveDebounce((keyword) => {
console.log('用', currentDelay, 'ms 的延迟搜索:', keyword);
});

// 检测到网络变慢
window.addEventListener('offline', () => {
adaptiveSearch.setDelay(100); // 离线或慢网时减少延迟,快速反馈
});

window.addEventListener('online', () => {
adaptiveSearch.setDelay(300); // 恢复默认
});

这种自适应防抖在移动端特别有用,4G 和 WiFi 切换时自动调整,用户体验更好。但注意,setDelay 只会影响下一次触发,已经 pending 的定时器不会变。

真实项目里怎么骚操作

光说不练假把式。搜素框联想这个经典场景,用防抖是基操,但怎么配合取消上一个请求,避免旧数据覆盖新数据,这里面的门道深着呢。还有那个无限滚动加载,节流用得不好,用户滚得快了直接白屏,滚慢了又频繁请求,怎么调参数才能既丝滑又省流量?甚至在一些拖拽排序、窗口 resize 监听里,这俩函数组合拳打好了,体验直接起飞。

搜索框的终极方案:防抖 + 请求取消 + 竞态处理

搜索框是防抖最经典的场景,但很多人只做了表面功夫。真正的生产环境要考虑:

  • 快速输入时取消旧请求
  • 防止旧请求晚返回覆盖新结果
  • 空值处理(用户删光内容时不搜索)
  • 加载状态管理
  • 错误重试
  • // React + TypeScript 完整版搜索组件
    import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
    import { debounce } from 'lodash';

    interface SearchResult {
    id: string;
    title: string;
    description: string;
    }

    interface UseSearchOptions {
    minLength?: number; // 最小搜索长度
    debounceMs?: number; // 防抖延迟
    maxWaitMs?: number; // 最大等待时间
    }

    function useSmartSearch(
    fetcher: (keyword: string) => Promise<SearchResult[]>,
    options: UseSearchOptions = {}
    ) {
    const {
    minLength = 1,
    debounceMs = 300,
    maxWaitMs = 1000
    } = options;

    const [keyword, setKeyword] = useState('');
    const [results, setResults] = useState<SearchResult[]>([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState<Error | null>(null);

    // 用 ref 存储最新的请求序号,处理竞态
    const requestIdRef = useRef(0);
    const abortControllerRef = useRef<AbortController | null>(null);

    // 创建防抖的搜索函数
    const debouncedSearch = useMemo(
    () => debounce(
    async (searchTerm: string, currentRequestId: number) => {
    // 长度不够不搜索
    if (searchTerm.length < minLength) {
    setResults([]);
    return;
    }

    setLoading(true);
    setError(null);

    // 取消上一次请求
    if (abortControllerRef.current) {
    abortControllerRef.current.abort();
    }

    abortControllerRef.current = new AbortController();

    try {
    const data = await fetcher(searchTerm);

    // 检查是否是最新请求,防止旧数据覆盖
    if (currentRequestId === requestIdRef.current) {
    setResults(data);
    } else {
    console.log('忽略过期的搜索结果');
    }
    } catch (err) {
    if (err.name === 'AbortError') return;

    if (currentRequestId === requestIdRef.current) {
    setError(err);
    }
    } finally {
    if (currentRequestId === requestIdRef.current) {
    setLoading(false);
    }
    }
    },
    debounceMs,
    { maxWait: maxWaitMs } // 最长等待 1 秒,防止一直输入不触发
    ),
    [fetcher, minLength, debounceMs, maxWaitMs]
    );

    // 监听 keyword 变化
    useEffect(() => {
    requestIdRef.current += 1;
    const currentId = requestIdRef.current;

    debouncedSearch(keyword, currentId);

    // 清理函数
    return () => {
    debouncedSearch.cancel();
    };
    }, [keyword, debouncedSearch]);

    // 组件卸载时彻底清理
    useEffect(() => {
    return () => {
    if (abortControllerRef.current) {
    abortControllerRef.current.abort();
    }
    };
    }, []);

    return {
    keyword,
    setKeyword,
    results,
    loading,
    error,
    // 手动刷新,不受防抖影响
    refresh: useCallback(() => {
    requestIdRef.current += 1;
    debouncedSearch(keyword, requestIdRef.current);
    debouncedSearch.flush(); // 立即执行
    }, [keyword, debouncedSearch])
    };
    }

    // 使用示例
    function SearchComponent() {
    const fetchSearchResults = async (keyword: string): Promise<SearchResult[]> => {
    const res = await fetch(`/api/search?q=${encodeURIComponent(keyword)}`);
    if (!res.ok) throw new Error('搜索失败');
    return res.json();
    };

    const {
    keyword,
    setKeyword,
    results,
    loading,
    error,
    refresh
    } = useSmartSearch(fetchSearchResults, {
    minLength: 2,
    debounceMs: 300,
    maxWaitMs: 800
    });

    return (
    <div className="search-container">
    <input
    type="text"
    value={keyword}
    onChange={(e) => setKeyword(e.target.value)}
    placeholder="输入关键词搜索…"
    className="search-input"
    />

    {loading && <div className="loading-spinner">加载中</div>}

    {error && (
    <div className="error-message">
    出错了: {error.message}
    <button onClick={refresh}>重试</button>
    </div>
    )}

    <ul className="results-list">
    {results.map(item => (
    <li key={item.id} className="result-item">
    <h4>{item.title}</h4>
    <p>{item.description}</p>
    </li>
    ))}
    </ul>

    {results.length === 0 && !loading && keyword.length >= 2 && (
    <div className="empty-state">暂无结果</div>
    )}
    </div>
    );
    }

    这个 Hook 的精髓在于 requestIdRef,每次 keyword 变化就自增,请求返回时检查 id 是否匹配,不匹配就直接丢弃。这比 AbortController 更可靠,因为 AbortController 只能取消请求,但如果请求已经在返回路上,取消不了,这时候 id 检查就能过滤掉旧数据。

    无限滚动加载:节流的参数调优艺术

    无限滚动是节流的经典场景,但参数调不好,要么卡成 PPT,要么疯狂请求把服务器打挂。

    // 无限滚动 Hook,带智能节流
    import { useEffect, useRef, useState, useCallback } from 'react';
    import { throttle } from 'lodash';

    function useInfiniteScroll(
    fetchMore: () => Promise<boolean>, // 返回是否还有更多数据
    options = {}
    ) {
    const {
    threshold = 100, // 距离底部多少像素触发
    throttleMs = 200, // 节流间隔
    maxRetries = 3 // 失败重试次数
    } = options;

    const [loading, setLoading] = useState(false);
    const [hasMore, setHasMore] = useState(true);
    const [error, setError] = useState(null);
    const retryCountRef = useRef(0);
    const containerRef = useRef(null);

    // 加载更多数据的包装函数
    const loadMore = useCallback(async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    setError(null);

    try {
    const more = await fetchMore();
    setHasMore(more);
    retryCountRef.current = 0; // 成功重置重试计数
    } catch (err) {
    console.error('加载失败:', err);
    setError(err);

    // 失败重试逻辑
    if (retryCountRef.current < maxRetries) {
    retryCountRef.current += 1;
    setTimeout(() => {
    setError(null);
    // 这里可以再次触发检查,或者等用户下次滚动
    }, 1000 * retryCountRef.current); // 指数退避
    }
    } finally {
    setLoading(false);
    }
    }, [fetchMore, loading, hasMore]);

    // 节流的滚动检查函数
    const throttledCheck = useMemo(
    () => throttle(() => {
    if (!containerRef.current) return;

    const container = containerRef.current;
    const scrollBottom = container.scrollTop + container.clientHeight;
    const height = container.scrollHeight;

    // 距离底部 threshold 像素时触发
    if (height scrollBottom < threshold) {
    loadMore();
    }
    }, throttleMs, {
    leading: false, // 滚动开始不立即执行,等停下来
    trailing: true // 滚动结束检查一次
    }),
    [loadMore, threshold, throttleMs]
    );

    useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    container.addEventListener('scroll', throttledCheck);

    // 初始检查,防止内容不够一屏
    throttledCheck();

    return () => {
    container.removeEventListener('scroll', throttledCheck);
    throttledCheck.cancel();
    };
    }, [throttledCheck]);

    // 暴露刷新方法
    const refresh = useCallback(() => {
    setHasMore(true);
    setError(null);
    retryCountRef.current = 0;
    loadMore();
    }, [loadMore]);

    return {
    containerRef,
    loading,
    hasMore,
    error,
    refresh
    };
    }

    // 使用示例
    function FeedList() {
    const [page, setPage] = useState(1);
    const [items, setItems] = useState([]);

    const fetchMore = async () => {
    const res = await fetch(`/api/feed?page=${page}&limit=20`);
    const data = await res.json();

    if (data.length === 0) return false; // 没有更多了

    setItems(prev => [prev, data]);
    setPage(p => p + 1);
    return true; // 可能还有更多
    };

    const { containerRef, loading, hasMore, error, refresh } = useInfiniteScroll(
    fetchMore,
    {
    threshold: 150, // 提前 150px 开始加载,无缝体验
    throttleMs: 150 // 150ms 检查一次,平衡性能和实时性
    }
    );

    return (
    <div ref={containerRef} className="feed-container" style={{ overflowY: 'auto', height: '100vh' }}>
    {items.map(item => (
    <FeedCard key={item.id} data={item} />
    ))}

    {loading && <div className="loading-more">加载中</div>}

    {!hasMore && <div className="no-more">到底了,别刷了</div>}

    {error && (
    <div className="error-load">
    加载失败 <button onClick={refresh}>点击重试</button>
    </div>
    )}
    </div>
    );
    }

    这里的参数 tuning 是关键。threshold 设太小,用户滑到底才加载,会看到白屏;设太大,提前加载太多,浪费流量。throttleMs 也是,设太小,滚动时频繁检查,CPU 占用高;设太大,可能错过触发时机。一般建议 threshold 100-200px,throttleMs 100-200ms,根据实际内容高度调整。

    拖拽排序:防抖节流的组合拳

    拖拽排序这种高频交互,需要同时用防抖和节流。节流控制位置更新的频率(60fps 流畅度),防抖处理最终的保存请求。

    // 拖拽排序组件,组合拳示例
    import React, { useState, useCallback, useRef } from 'react';
    import { throttle, debounce } from 'lodash';

    function SortableList({ items: initialItems, onReorder }) {
    const [items, setItems] = useState(initialItems);
    const [draggingId, setDraggingId] = useState(null);
    const [dragOverId, setDragOverId] = useState(null);

    // 节流:拖拽时实时更新位置,限制 60fps(16ms)
    const throttledMove = useMemo(
    () => throttle((fromId, toId) => {
    setItems(prev => {
    const fromIndex = prev.findIndex(i => i.id === fromId);
    const toIndex = prev.findIndex(i => i.id === toId);

    if (fromIndex === 1 || toIndex === 1) return prev;

    const newItems = [prev];
    const [moved] = newItems.splice(fromIndex, 1);
    newItems.splice(toIndex, 0, moved);

    return newItems;
    });
    }, 16),
    []
    );

    // 防抖:拖拽结束后保存,等 500ms 确认没再拖了再发请求
    const debouncedSave = useMemo(
    () => debounce((newItems) => {
    console.log('保存新顺序到服务器:', newItems.map(i => i.id));
    onReorder(newItems);
    }, 500),
    [onReorder]
    );

    const handleDragStart = useCallback((e, id) => {
    setDraggingId(id);
    e.dataTransfer.effectAllowed = 'move';
    // 设置拖拽图像(可选)
    const img = new Image();
    img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
    e.dataTransfer.setDragImage(img, 0, 0);
    }, []);

    const handleDragOver = useCallback((e, id) => {
    e.preventDefault();
    if (id === draggingId || id === dragOverId) return;

    setDragOverId(id);
    // 节流更新视觉顺序,流畅但不卡
    throttledMove(draggingId, id);
    }, [draggingId, dragOverId, throttledMove]);

    const handleDrop = useCallback((e) => {
    e.preventDefault();
    setDraggingId(null);
    setDragOverId(null);

    // 防抖保存,避免频繁请求
    debouncedSave(items);
    }, [items, debouncedSave]);

    const handleDragEnd = useCallback(() => {
    setDraggingId(null);
    setDragOverId(null);
    // 如果没触发 drop,也要保存
    debouncedSave.flush(); // 立即执行,不等了
    }, [debouncedSave]);

    return (
    <ul className="sortable-list" onDragEnd={handleDragEnd}>
    {items.map((item, index) => (
    <li
    key={item.id}
    draggable
    onDragStart={(e) => handleDragStart(e, item.id)}
    onDragOver={(e) => handleDragOver(e, item.id)}
    onDrop={handleDrop}
    className={`sortable-item ${draggingId === item.id ? 'dragging' : ''} ${dragOverId === item.id ? 'drag-over' : ''}`}
    style={{
    transform: draggingId === item.id ? 'scale(1.02)' : 'none',
    transition: 'transform 0.1s'
    }}
    >
    <span className="drag-handle"></span>
    <span className="item-index">{index + 1}.</span>
    <span className="item-content">{item.content}</span>
    </li>
    ))}
    </ul>
    );
    }

    // 使用
    function App() {
    const [items, setItems] = useState([
    { id: '1', content: '学习 React' },
    { id: '2', content: '学习 TypeScript' },
    { id: '3', content: '学习 Node.js' },
    { id: '4', content: '学习设计模式' }
    ]);

    const handleReorder = useCallback(async (newItems) => {
    // 这里可以发请求保存顺序
    await fetch('/api/reorder', {
    method: 'POST',
    body: JSON.stringify({ ids: newItems.map(i => i.id) }),
    headers: { 'Content-Type': 'application/json' }
    });
    }, []);

    return <SortableList items={items} onReorder={handleReorder} />;
    }

    这个例子展示了如何组合使用:throttle 处理高频的拖拽移动(16ms 约等于 60fps),debounce 处理低频的保存操作(500ms)。这样既保证了拖拽的流畅度,又避免了服务器压力。

    窗口 Resize:性能杀手怎么治

    窗口 resize 是性能重灾区,如果不做处理,连续触发几百次,重排重绘能把页面卡死。

    // 响应式布局的 Resize 处理
    import { useEffect, useState, useMemo } from 'react';
    import { debounce, throttle } from 'lodash';

    function useResponsive() {
    const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
    });

    const [breakpoint, setBreakpoint] = useState('desktop');

    // 节流:实时更新尺寸,限制频率
    const throttledUpdateSize = useMemo(
    () => throttle(() => {
    setWindowSize({
    width: window.innerWidth,
    height: window.innerHeight
    });
    }, 100), // 100ms 更新一次尺寸数据
    []
    );

    // 防抖:判断断点变化,等 resize 结束再确定
    const debouncedUpdateBreakpoint = useMemo(
    () => debounce((width) => {
    if (width < 768) {
    setBreakpoint('mobile');
    } else if (width < 1024) {
    setBreakpoint('tablet');
    } else {
    setBreakpoint('desktop');
    }

    // 可以在这里触发布局重计算
    console.log('当前断点:', width < 768 ? 'mobile' : width < 1024 ? 'tablet' : 'desktop');
    }, 150),
    []
    );

    useEffect(() => {
    const handleResize = () => {
    throttledUpdateSize();
    debouncedUpdateBreakpoint(window.innerWidth);
    };

    window.addEventListener('resize', handleResize);

    // 初始化一次
    handleResize();

    return () => {
    window.removeEventListener('resize', handleResize);
    throttledUpdateSize.cancel();
    debouncedUpdateBreakpoint.cancel();
    };
    }, [throttledUpdateSize, debouncedUpdateBreakpoint]);

    return {
    windowSize,
    breakpoint,
    isMobile: breakpoint === 'mobile',
    isTablet: breakpoint === 'tablet',
    isDesktop: breakpoint === 'desktop'
    };
    }

    // 使用:自适应组件
    function AdaptiveLayout() {
    const { width, height, breakpoint, isMobile } = useResponsive();

    return (
    <div className={`layout ${breakpoint}`}>
    <header>
    <h1>当前窗口: {width}x{height}</h1>
    <p>断点: {breakpoint}</p>
    </header>

    <main style={{
    display: 'grid',
    gridTemplateColumns: isMobile ? '1fr' : 'repeat(3, 1fr)',
    gap: '20px',
    transition: 'grid-template-columns 0.3s' // 平滑过渡
    }}>
    <div className="card">内容 1</div>
    <div className="card">内容 2</div>
    <div className="card">内容 3</div>
    </main>
    </div>
    );
    }

    这里用了双重保险:throttle 保证尺寸数据实时但不频繁更新,debounce 保证断点判断在 resize 结束后才最终确定。如果只用 throttle,resize 过程中会频繁判断断点,可能导致布局抖动;如果只用 debounce,resize 过程中完全没有反馈,用户看不到实时变化。

    遇到 Bug 别只会重启

    有时候代码明明没毛病,就是不生效。这时候别急着删库跑路。先看看是不是上下文 this 指飞了,特别是在回调函数里;再查查定时器有没有被意外清除,或者多个实例互相干扰。如果是用了异步 async/await,更要小心,别让 Promise 的状态把执行顺序搞乱了。学会用浏览器的 Performance 面板抓时间线,一眼就能看出是哪个环节掉了链子。

    调试技巧一:Devtools Performance 面板

    打开 Chrome DevTools,切到 Performance 面板,录制一段交互,你能看到:

  • Task:看是否有长任务(Long Task),如果有,说明防抖节流没生效,主线程被阻塞了
  • Function Call:展开后能看到 debounce/throttle 内部函数的调用频率
  • Timer Fired:看 setTimeout/setInterval 的触发情况,检查定时器是否按预期工作
  • // 给 debounce 函数打标记,方便在 Performance 面板识别
    function debugDebounce(fn, delay) {
    let timer;

    return function(args) {
    // 在控制台标记,Performance 面板能看到
    console.timeStamp('debounce triggered');

    clearTimeout(timer);
    timer = setTimeout(() => {
    console.timeStamp('debounce executed');
    fn.apply(this, args);
    }, delay);
    };
    }

    // 使用
    const test = debugDebounce(() => {
    console.log('真正执行');
    }, 1000);

    // 快速点击 5 次
    for (let i = 0; i < 5; i++) {
    setTimeout(() => test(i), i * 100);
    }
    // 预期:只看到一次 'debounce executed' 和 '真正执行'

    调试技巧二:检查多个实例

    React 中最常见的 bug 是创建了多个 debounce 实例,每个都有自己的定时器,结果就乱套了。

    // 错误示范:每次渲染都创建新实例
    function BadComponent() {
    const [count, setCount] = useState(0);

    // 错误!每次渲染都新的 debounce
    const handleClick = debounce(() => {
    console.log('点击了', count);
    }, 1000);

    return <button onClick={handleClick}>点我</button>;
    }

    // 怎么发现这个问题?加日志
    function BadComponentDebug() {
    const [count, setCount] = useState(0);

    const handleClick = useMemo(() => {
    console.log('创建新的 debounce 实例'); // 如果频繁打印,说明有问题
    return debounce(() => {
    console.log('执行,count=', count);
    }, 1000);
    }, [count]); // 依赖 count 导致每次更新都重建

    return <button onClick={handleClick}>点我</button>;
    }

    // 正确做法:空依赖数组,或者用 ref
    function GoodComponent() {
    const countRef = useRef(count);
    countRef.current = count; // 保持最新值

    const handleClick = useMemo(() => {
    return debounce(() => {
    console.log('执行,count=', countRef.current); // 通过 ref 获取最新值
    }, 1000);
    }, []); // 空依赖,只创建一次

    return <button onClick={handleClick}>点我</button>;
    }

    调试技巧三:内存泄漏检测

    如果组件反复挂载卸载,debounce 的定时器没清理,会导致内存泄漏。用 Chrome 的 Memory 面板可以检测:

  • 打开 Memory 面板
  • 点击"Take heap snapshot"
  • 执行一系列操作(比如打开关闭弹窗 10 次)
  • 再拍一张快照
  • 对比两个快照,搜索你的组件名或 debounce 相关对象
  • 如果发现实例数量只增不减,说明有泄漏。检查 useEffect 的 cleanup 函数是否调用了 cancel。

    // 安全的 Hook 封装,带内存泄漏防护
    function useSafeDebounce(fn, delay, deps = []) {
    const fnRef = useRef(fn);
    fnRef.current = fn;

    const debouncedFn = useMemo(
    () => debounce((args) => fnRef.current(args), delay),
    [delay] // 只依赖 delay
    );

    useEffect(() => {
    return () => {
    debouncedFn.cancel();
    };
    }, [debouncedFn]);

    return debouncedFn;
    }

    // 使用
    function SafeComponent() {
    const [text, setText] = useState('');

    const debouncedSearch = useSafeDebounce((value) => {
    console.log('搜索:', value);
    }, 500);

    return (
    <input
    value={text}
    onChange={(e) => {
    setText(e.target.value);
    debouncedSearch(e.target.value);
    }}
    />
    );
    }

    几个让同事喊爸爸的 trick

    除了调库,咱还得有点私货。比如怎么封装一个通用的 Hook,在 React 或 Vue 里一行代码搞定防抖;怎么利用函数的柯里化,把配置项预设好,让业务代码清爽得像刚洗过的衬衫。还有啊,别死守着默认参数,根据网络状况动态调整延迟时间,这种小细节才是区分初级和高级开发的分水岭。

    Trick 1:通用 Hook 封装,一行代码搞定

    // 终极版 useDebounce Hook,支持所有选项
    import { useMemo, useEffect, useRef } from 'react';
    import { debounce, throttle, DebouncedFunc, ThrottledFunc } from 'lodash';
    import type { DebounceSettings, ThrottleSettings } from 'lodash';

    type UseDebounceOptions = DebounceSettings & {
    immediate?: boolean; // 是否立即执行
    };

    export function useDebounce<T extends (args: any[]) => any>(
    fn: T,
    delay: number = 300,
    options: UseDebounceOptions = {}
    ): DebouncedFunc<T> & { cancel: () => void; flush: () => void } {
    const { immediate = false, debounceOptions } = options;
    const fnRef = useRef(fn);
    fnRef.current = fn;

    const debounced = useMemo(() => {
    const debouncedFn = debounce(
    (args: Parameters<T>) => fnRef.current(args),
    delay,
    {
    leading: immediate, // 立即执行用 leading
    trailing: !immediate, // 非立即执行用 trailing
    debounceOptions
    }
    );

    return debouncedFn;
    }, [delay, immediate, Object.values(debounceOptions)]);

    useEffect(() => {
    return () => {
    debounced.cancel();
    };
    }, [debounced]);

    return debounced as any;
    }

    // 对应的 useThrottle
    export function useThrottle<T extends (args: any[]) => any>(
    fn: T,
    interval: number = 200,
    options: ThrottleSettings = {}
    ): ThrottledFunc<T> & { cancel: () => void; flush: () => void } {
    const fnRef = useRef(fn);
    fnRef.current = fn;

    const throttled = useMemo(() => {
    return throttle(
    (args: Parameters<T>) => fnRef.current(args),
    interval,
    { leading: true, trailing: false, options }
    );
    }, [interval, Object.values(options)]);

    useEffect(() => {
    return () => throttled.cancel();
    }, [throttled]);

    return throttled as any;
    }

    // 使用示例,简单到离谱
    function SearchInput() {
    const [value, setValue] = useState('');

    // 一行代码搞定,还能自动清理
    const debouncedSearch = useDebounce(
    (keyword: string) => {
    console.log('搜索:', keyword);
    // 发请求…
    },
    500,
    { maxWait: 2000 } // 最长等 2 秒
    );

    return (
    <input
    value={value}
    onChange={(e) => {
    setValue(e.target.value);
    debouncedSearch(e.target.value);
    }}
    placeholder="输入搜索关键词…"
    />
    );
    }

    这个 Hook 的精髓在于:

  • 用 ref 保持函数引用最新,避免闭包陷阱
  • 自动处理 cancel,防止内存泄漏
  • TypeScript 类型完整,有智能提示
  • 支持所有 lodash 的选项
  • Trick 2:柯里化预设配置

    如果你在一个项目里到处都用同样的防抖配置,可以用柯里化封装:

    // 创建预设的防抖函数工厂
    const createProjectDebounce = (defaultDelay = 300) => {
    return (fn, customDelay, customOptions) => {
    return debounce(fn, customDelay || defaultDelay, {
    maxWait: 1000, // 项目统一配置
    leading: false,
    trailing: true,
    customOptions // 允许覆盖
    });
    };
    };

    // 创建项目级实例
    const projectDebounce = createProjectDebounce(300);

    // 使用,代码更清爽
    const search = projectDebounce((keyword) => {
    api.search(keyword);
    });

    const saveDraft = projectDebounce((content) => {
    api.saveDraft(content);
    }, 1000); // 覆盖延迟为 1 秒

    const urgentSave = projectDebounce((content) => {
    api.save(content);
    }, 0, { leading: true, trailing: false }); // 完全自定义

    Trick 3:网络自适应延迟

    根据网络状况动态调整防抖时间,WiFi 时延迟高点省流量,4G 时延迟低点快速反馈。

    // 网络状态感知防抖
    function useNetworkAwareDebounce(fn, options = {}) {
    const { fastDelay = 100, slowDelay = 500 } = options;
    const [delay, setDelay] = useState(slowDelay);

    useEffect(() => {
    // 监听网络变化
    const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;

    if (connection) {
    const updateConnectionStatus = () => {
    // effectiveType: '4g', '3g', '2g', 'slow-2g'
    const effectiveType = connection.effectiveType;

    if (effectiveType === '4g' && !connection.saveData) {
    setDelay(fastDelay); // 网速快,延迟低点
    } else {
    setDelay(slowDelay); // 网速慢或省流量模式,延迟高点
    }
    };

    connection.addEventListener('change', updateConnectionStatus);
    updateConnectionStatus(); // 初始检查

    return () => connection.removeEventListener('change', updateConnectionStatus);
    }
    }, [fastDelay, slowDelay]);

    return useDebounce(fn, delay, options);
    }

    // 使用
    function SmartSearch() {
    const search = useNetworkAwareDebounce(
    (keyword) => api.search(keyword),
    { fastDelay: 150, slowDelay: 600 }
    );

    return <input onChange={(e) => search(e.target.value)} />;
    }

    Trick 4:组合键防抖

    处理键盘快捷键时,需要特殊处理,比如 Ctrl+S 保存,要防止按住不放时疯狂触发。

    // 键盘快捷键防抖
    function useKeyboardShortcut(keyCombo, callback, delay = 300) {
    const pressedKeys = useRef(new Set());

    const debouncedCallback = useDebounce(callback, delay, {
    leading: true, // 第一次立即执行
    trailing: false // 不补执行
    });

    useEffect(() => {
    const handleKeyDown = (e) => {
    const key = e.key.toLowerCase();
    pressedKeys.current.add(key);

    // 检查组合键是否匹配
    const keys = keyCombo.toLowerCase().split('+');
    const allPressed = keys.every(k => pressedKeys.current.has(k));

    if (allPressed) {
    e.preventDefault();
    debouncedCallback();
    }
    };

    const handleKeyUp = (e) => {
    pressedKeys.current.delete(e.key.toLowerCase());
    };

    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);

    return () => {
    window.removeEventListener('keydown', handleKeyDown);
    window.removeEventListener('keyup', handleKeyUp);
    };
    }, [keyCombo, debouncedCallback]);
    }

    // 使用:Ctrl+S 保存,即使按住也只执行一次
    function Editor() {
    useKeyboardShortcut('ctrl+s', () => {
    console.log('保存文档…');
    saveDocument();
    }, 500);

    return <textarea />;
    }

    最后碎碎念

    行了,差不多就唠到这。其实工具再好,也就是个辅助,关键还是得理解背后的原理,不然换个场景照样抓瞎。下次要是再看到谁在项目里手写十几行的防抖函数,记得把这篇文章甩他脸上,让他知道什么叫"站在巨人的肩膀上摸鱼"。

    说到底,防抖和节流这俩玩意儿,前端面试问了十年,项目里用了十年,但每年还是有新人踩坑。为啥?因为光看概念简单,真到工程实践里,要考虑边界情况、内存管理、异步处理、框架集成,复杂度直接翻倍。

    2026 年了,lodash 依然是稳妥的选择,但如果你对包大小敏感,throttle-debounce 这种轻量库也够用了。RxJS 适合已经在用响应式编程的项目,WASM 版本除非极端性能需求否则不建议折腾。

    最后送大家一句话:别重复造轮子,除非你能造得更好;也别盲目用库,除非你理解它在干嘛。技术选型没有银弹,只有适合当前场景的解决方案。咱们下期再见,拜拜了您嘞!

    欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

    推荐:DTcode7的博客首页。 一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


    专栏系列(点击解锁)学习路线(点击解锁)知识定位
    《微信小程序相关博客》 持续更新中~ 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
    《AIGC相关博客》 持续更新中~ AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
    《HTML网站开发相关》 《前端基础入门三大核心之html相关博客》 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
    《前端基础入门三大核心之JS相关博客》 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
    《前端基础入门三大核心之CSS相关博客》 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
    《canvas绘图相关博客》 Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
    《Vue实战相关博客》 持续更新中~ 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
    《python相关博客》 持续更新中~ Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
    《sql数据库相关博客》 持续更新中~ SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
    《算法系列相关博客》 持续更新中~ 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
    《IT信息技术相关博客》 持续更新中~ 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
    《信息化人员基础技能知识相关博客》 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
    《信息化技能面试宝典相关博客》 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
    《前端开发习惯与小技巧相关博客》 持续更新中~ 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
    《photoshop相关博客》 持续更新中~ 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
    日常开发&办公&生产【实用工具】分享相关博客》 持续更新中~ 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

    吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤 非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 前端老铁别硬扛:手写防抖节流太累,2026年主流库实战避坑指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址