防抖与节流:从输入框看性能优化

0 阅读9分钟

防抖与节流:从输入框看性能优化

前言

在前端开发中,我们经常会遇到一些高频触发的事件——比如输入框的 keyup、窗口的 resize、页面的 scroll 等。如果每次事件都立即执行回调,往往会带来性能问题:频繁的 AJAX 请求加重服务器负担,复杂的 DOM 计算导致页面卡顿,甚至影响用户体验。

为了解决这类问题,前端工程师引入了两个经典的优化工具:防抖(debounce)节流(throttle)。它们虽然名字相似,但原理和应用场景截然不同。很多初学者容易混淆这两个概念,或者只会用现成的库函数却不理解其内部机制。

本文将从一段最朴素的输入框代码开始,一步步带你发现性能问题的根源,然后通过手写防抖和节流函数(基于你提供的代码),并提供完整的可运行 HTML 示例,逐行注释解析它们的实现原理。最后通过对比表格和场景分析,帮你彻底搞懂这两个工具的区别与选择。你可以直接复制文中的 HTML 代码到本地运行,亲眼观察效果。


一、从一个简单输入框开始(无优化)

假设我们有一个搜索框,希望在用户输入时实时显示建议(suggest)。最直接的写法是监听 keyup 事件,每次按键都发起 AJAX 请求(这里用 console.log 模拟请求)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>无防抖/节流示例</title>
</head>
<body>
    <h3>无防抖/节流:每次按键都会触发</h3>
    <input type="text" id="undebounce" placeholder="输入内容" />

    <script>
        // 模拟 AJAX 请求
        function ajax(content) {
            console.log('ajax request', content);
        }

        const inputa = document.getElementById('undebounce');
        inputa.addEventListener('keyup', function(e) {
            ajax(e.target.value); // 每次按键都调用
        });
    </script>
</body>
</html>

将以上代码保存为 HTML 文件并在浏览器中打开,打开控制台(F12),然后在输入框中快速输入文字,你会看到每按一个键控制台就打印一条日志,瞬间输出大量内容。

效果图

屏幕录制 2026-02-26 212156.gif

这样写会有什么问题?
当用户快速输入时(比如一秒内按下 10 个键),就会连续发起 10 个请求,而其中绝大部分请求是没必要的(因为用户还没输完)。这不仅浪费带宽,还会给服务器造成压力,甚至导致页面卡顿。

为了解决这类问题,我们需要一种机制,控制函数的执行频率——这就是防抖和节流的用武之地。


二、防抖(debounce):只执行最后一次

2.1 什么是防抖?

防抖的核心思想是:当你频繁触发事件时,只在最后一次触发后的指定时间后执行一次。如果在这段时间内再次触发,则重新计时。

还是拿搜索框举例:我们希望用户停止输入一段时间(比如 500ms)后再去请求建议,而不是每敲一个字都请求。

2.2 手写一个防抖函数(带详细注释)

下面是一个完整的 HTML 示例,它使用了用户提供的防抖函数,并加上了详细的注释。你可以直接运行并观察效果。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>防抖示例</title>
</head>
<body>
    <h3>防抖:停止输入 500ms 后触发</h3>
    <input type="text" id="debounce" placeholder="输入内容" />

    <script>
        // 模拟 AJAX 请求
        function ajax(content) {
            console.log('ajax request', content);
        }

        /**
         * 防抖函数
         * @param {Function} fn 需要执行的函数
         * @param {number} delay 延迟时间(毫秒)
         * @returns {Function} 返回一个具有防抖功能的新函数
         */
        function debounce(fn, delay) {
            // 利用闭包保存定时器ID,这样每次调用返回的函数时都能访问同一个变量
            var id;

            // 返回的函数就是实际绑定到事件上的处理函数
            return function(args) {
                // 如果已经有定时器,说明之前有过触发,取消它(重新计时)
                if (id) {
                    clearTimeout(id);
                }

                // 保存当前的 this 上下文,因为在 setTimeout 回调中 this 会丢失
                var that = this;

                // 设置一个新的定时器,delay 毫秒后执行
                id = setTimeout(function() {
                    // 在定时器回调中,通过 call 调用原函数,并传入正确的 this 和参数
                    fn.call(that, args);
                }, delay);
            };
        }

        const inputb = document.getElementById('debounce');
        // 创建一个防抖版本的 ajax 函数,延迟 500ms
        const debounceAjax = debounce(ajax, 500);

        inputb.addEventListener('keyup', function(e) {
            // 每次触发都调用防抖函数,参数是输入框当前值
            debounceAjax(e.target.value);
        });
    </script>
</body>
</html>

运行说明
在输入框中快速输入一串文字,然后停止。你会发现只有在停止输入 500ms 后,控制台才会打印一次请求内容。即使你输入过程中按键频率很高,也只会触发最后一次。

效果图

快速输入后停止,控制台只有一条输出的截图

屏幕录制 2026-02-26 212613.gif

2.3 防抖逻辑可视化

假设用户快速输入了三次,每次间隔 200ms,delay=500ms

  • 第一次输入(0ms)id 为空,跳过 if,设置定时器 A,计划在 500ms 后执行。
  • 第二次输入(200ms):清除定时器 A,设置定时器 B,计划在 700ms 后执行(200+500)。
  • 第三次输入(400ms):清除定时器 B,设置定时器 C,计划在 900ms 后执行(400+500)。
  • 900ms 时:定时器 C 执行,调用 fn

结果:只执行了一次,且是最后一次输入后的 500ms。

2.4 防抖的适用场景

  • 搜索框建议
  • 窗口 resize(等待调整完成后再计算)
  • 表单验证(输入完成后验证)
  • 按钮提交(防止重复提交,常结合立即执行选项)

三、节流(throttle):控制执行频率

3.1 什么是节流?

节流的核心思想是:无论事件触发的频率有多高,保证在单位时间内只执行一次。就像FPS游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。

3.2 手写一个节流函数(用户提供的版本,带详细注释)

下面是一个完整的 HTML 示例,它使用了用户提供的节流函数,并加上了详细的注释。你可以直接运行并观察效果。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>节流示例(用户版本)</title>
</head>
<body>
    <h3>节流:第一次立即执行,停止后执行最后一次</h3>
    <input type="text" id="throttle" placeholder="输入内容" />

    <script>
        // 模拟 AJAX 请求
        function ajax(content) {
            console.log('发送请求,内容:', content);
        }

        /**
         * 节流函数(用户提供的特殊版本:首次立即执行,连续触发只执行最后一次)
         * @param {Function} fn 需要执行的函数
         * @param {number} delay 间隔时间(毫秒)
         * @returns {Function} 返回一个具有节流功能的新函数
         */
        function throttle(fn, delay) {
            // last: 上次执行函数的时间戳(毫秒)
            // deferTimer: 定时器ID,用于延迟执行
            let last, deferTimer;

            // 返回的函数是实际的事件处理函数
            return function() {
                // 保存当前的 this 上下文,因为下面有 setTimeout
                let that = this;

                // 保存所有传入的参数(arguments 是类数组对象)
                let _args = arguments;

                // 获取当前时间戳,+new Date() 等效于 new Date().getTime()
                let now = +new Date();

                // 判断是否处于“冷却期”:
                // last 存在(即已经执行过至少一次)并且 当前时间 < 上次执行时间 + 间隔
                if (last && now < last + delay) {
                    // 还在冷却期内:清除之前设置的定时器(如果有)
                    clearTimeout(deferTimer);

                    // 设置一个新的定时器,在 delay 毫秒后执行
                    deferTimer = setTimeout(function() {
                        // 到达执行时刻:将 last 更新为触发时刻的时间戳(注意 now 是外层的值)
                        last = now;
                        // 使用 apply 调用原函数,传入保存的 this 和参数,停止输入后还要执行最后一次
                        fn.apply(that, _args);
                    }, delay);
                } else {
                    // 首次调用 或 冷却期已过:立即执行
                    last = now;                 // 更新上次执行时间为当前时间戳
                    fn.apply(that, _args);      // 立即执行原函数
                }
            };
        }

        const inputc = document.getElementById('throttle');
        // 创建一个节流版本的 ajax 函数,间隔 500ms
        const throttleAjax = throttle(ajax, 500);

        inputc.addEventListener('keyup', function(e) {
            throttleAjax(e.target.value);
        });
    </script>
</body>
</html>

运行说明

  • 在输入框中第一次按键,会立即看到控制台打印(第一次立即执行)。
  • 接着快速连续输入,在输入过程中不会有任何打印。
  • 停止输入后,等待 500ms,控制台会再打印一次(最后一次延迟执行)。

效果图:第一次立即执行一次,每隔相同时间执行一次,停止后还会执行最后一次

屏幕录制 2026-02-26 212613.gif

3.7 节流的适用场景

  • 滚动加载(监听滚动位置)
  • 鼠标移动、拖拽(如 Canvas 画笔)
  • 播放进度条更新
  • 按钮防连点(视需求可选防抖或节流)

四、防抖 vs 节流:一张表看懂区别

特性防抖(debounce)节流(throttle)
执行策略只执行最后一次每隔一段时间执行一次
典型实现每次触发重置定时器使用时间戳或定时器锁
代码示例debounce(ajax, 500)throttle(ajax, 500)
行为描述疯狂输入后停 500ms 执行一次第一次立即执行,之后每 500ms 至少一次
核心逻辑清除之前的定时器,重新设置判断时间间隔或锁状态
适合场景输入搜索、窗口 resize、表单验证滚动加载、鼠标移动、动画控制
类比电梯关门技能冷却

五、如何选择?

  • 当你关心最终状态时,用防抖(比如用户最终输入了什么)。
  • 当你需要持续反馈但又要控制频率时,用节流(比如滚动位置、鼠标移动)。

在实际项目中,Lodash 提供了成熟的 _.debounce_.throttle 函数,支持更多选项(如立即执行、取消等)。但理解手写实现能帮你更透彻地掌握闭包、定时器和 this 的用法。


六、总结

  • 防抖:只执行最后一次,适合输入、调整等场景。
  • 节流:均匀执行,适合滚动、动画等场景。
  • 两者都是通过闭包保存状态(定时器ID/上次执行时间)来控制执行频率。
  • 用户提供的防抖代码清晰展示了闭包和定时器的配合;节流代码则演示了一种特殊的行为(首次立即 + 最后一次延迟),理解它有助于区分不同实现间的细微差别。

希望本文能帮你理清防抖和节流的原理与区别,现在就去优化你的项目吧!


如果你觉得本文有帮助,欢迎点赞收藏,有任何问题可以在评论区交流~