前端性能优化魔法:防抖与节流,让你的应用“飞”起来!🚀

61 阅读15分钟

好的,编程爱好者们,准备好了吗?今天咱们要聊点前端性能优化里的“硬核”知识,但别担心,我会用最轻松欢快的方式,带你玩转防抖(Debounce)节流(Throttle) 这两大“神兵利器”!🚀

⭐性能优化的魔法棒——防抖与节流

想象一下,你正在一个电商网站上愉快地购物,突然想搜索一件心仪的商品。你在搜索框里噼里啪啦地输入“机械键盘”,每敲一个字母,页面就“卡顿”一下,甚至还可能发出好几次请求,这体验是不是瞬间就不香了?又或者,你在刷微博、逛知乎,手指在屏幕上飞快地滑动,结果页面滚动得一卡一卡的,图片半天加载不出来,是不是想摔手机?📱

这些糟糕的用户体验,很多时候都源于我们对某些高频事件处理不当。在前端开发中,像keyup(键盘抬起)、resize(窗口大小改变)、scroll(页面滚动)、mousemove(鼠标移动)等事件,都可能在短时间内被触发无数次。如果每次触发都执行一些耗时操作(比如发送网络请求、DOM操作),那我们的应用性能就会直线下降,用户体验自然也跟着“跳水”了。

别慌!今天的主角——防抖(Debounce)节流(Throttle),就是来拯救我们的“性能危机”的!它们就像两根魔法棒,能巧妙地控制事件的执行频率,让我们的应用既能响应用户的操作,又不会因为过于频繁的执行而拖垮性能。

本文的目标就是:深入浅出,带你彻底理解防抖与节流的原理、实现和应用场景,让你也能成为前端性能优化的“魔法师”!✨

初探“频繁触发”之痛:inputa 的烦恼

为了更好地理解防抖和节流的价值,我们先来看一个“反面教材”——一个没有经过任何优化的输入框。

假设我们有一个简单的HTML页面,里面有三个输入框,以及一个模拟发送Ajax请求的函数:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>防抖与节流</title>
</head>
<body>
    <div>
        <input type="text" id="undebounce" placeholder="未防抖/节流的输入框" />
        <br>
        <input type="text" id="debounce" placeholder="防抖输入框" />
        <br>
        <input type="text" id="throttle" placeholder="节流输入框" />
    </div>
    <script>
    function ajax(content) {
        console.log('ajax request',content);
        // 实际项目中这里会是真正的网络请求,比如 axios.get('/search?q=' + content)
    }
    // ... 后面会添加防抖和节流的实现
    </script>
</body>
</html>

我们重点关注第一个输入框 undebounce 的事件处理逻辑:

const inputa = document.getElementById('undebounce');
// 频繁触发
inputa.addEventListener('keyup',function(e) {
    ajax(e.target.value); //蛮复杂
});

这段代码非常直观:当用户在 inputa 输入框中每敲击并抬起一次键盘(keyup事件),我们就会立即调用 ajax 函数,模拟发送一次网络请求。

现在,请你想象一下,如果你快速地输入“hello world”,你会触发多少次 keyup 事件?没错,至少10次!这意味着,在用户输入的过程中,我们的 ajax 函数会被调用10次,向服务器发送10次请求!

这在实际项目中简直是灾难!😱

  • 服务器压力山大:短时间内接收大量重复或无效的请求,服务器表示“我太难了!”。
  • 用户体验极差:频繁的网络请求可能导致页面卡顿、响应变慢,用户会觉得应用“不流畅”。
  • 资源浪费:很多中间的请求其实是无效的,因为用户最终只关心最后一次输入的结果。

这就是“频繁触发”之痛!为了解决这个问题,我们的“救星”——防抖,就要登场了!

防抖(Debounce):“我只关心最后一次!”

防抖是什么?

防抖,顾名思义,就是防止抖动。它的核心思想是:当事件被触发后,不立即执行回调函数,而是等待一段时间。如果在这段时间内事件再次被触发,则重新计时。直到事件在指定的时间内不再被触发,才执行最终的回调函数。

用一个形象的比喻来说,防抖就像是“电梯关门”: 当你按下电梯按钮后,电梯门不会立即关闭,而是会等待几秒。如果在这几秒内,又有人按了按钮,电梯门会重新计时,再次等待几秒。只有当电梯门等待的这段时间内,没有人再按按钮了,电梯门才会真正关闭。它只关心“最后一次”的指令。

常见的应用场景

  • 搜索框输入:用户在搜索框中输入内容时,我们通常不希望每输入一个字符就发送一次搜索请求,而是希望用户停止输入一段时间后,再发送最终的搜索请求。
  • 窗口resize事件:当浏览器窗口大小改变时,resize事件会频繁触发。如果我们在resize事件中执行复杂的布局计算,会导致页面卡顿。使用防抖可以确保只在窗口停止改变大小后,才执行一次布局操作。
  • 表单提交:防止用户在短时间内多次点击提交按钮,导致重复提交表单。
  • code suggest:在用户输入代码时,编辑器通常会实时提供智能提示(如变量名、函数名、语法建议等)。如何频繁提示会不断向LLM发送请求请求开销过大。

核心代码解析:debounce 函数

现在,让我们来看看如何用代码实现防抖功能。我们回到debounce 函数:

// 高阶函数 参数或者返回值(闭包)是函数(函数就是对象)
function debounce(fn,delay) {
    var id;// 自由变量
    return function(args) {
        if(id) clearTimeout(id); // 核心逻辑:如果定时器存在,就清除它
        var that = this; // 保存当前函数的this上下文
        id = setTimeout(function(){
            fn.call(that,args); // 使用call/apply确保this指向正确,并传递参数
        }, delay);
    }
}

这段代码虽然短小精悍,但却蕴含了几个重要的JavaScript概念:

  1. 高阶函数(Higher-Order Function)debounce 函数就是一个典型的高阶函数。它接收一个函数 fn(我们真正要执行的业务逻辑,比如 ajax)作为参数,并且返回一个新的函数。这种 “函数接收函数,或者返回函数” 的特性,就是高阶函数的标志。高阶函数让我们的代码更加灵活和可复用。

  2. 闭包(Closure)debounce 函数内部的 id 变量,以及它返回的那个匿名函数,共同形成了一个闭包。 当 debounce 函数执行完毕并返回内部函数时,即使 debounce 函数的执行上下文已经销毁,但内部函数依然能够访问到 debounce 函数作用域内的 id 变量。这个 id 变量被“私有化”地保存在闭包中,每次调用返回的函数时,都能操作同一个 id。这是防抖实现的关键!

  3. var id; —— 自由变量的妙用id 变量在这里扮演了“定时器ID”的角色。它是一个“自由变量”,因为它不是在返回的匿名函数内部声明的,而是从外部作用域(debounce 函数)捕获而来的。

    • if(id) clearTimeout(id);:这是防抖的核心逻辑!每次返回的函数被调用时,它都会检查 id 是否存在(即之前是否设置过定时器)。如果存在,就立即使用 clearTimeout 取消掉上一次设置的定时器。这意味着,只要在 delay 时间内再次触发事件,上一次的定时器就会被“作废”,不会执行。
    • id = setTimeout(...):然后,它会重新设置一个新的定时器,并将新的定时器ID赋值给 id。这样,只有当 delay 时间过去,并且在这段时间内没有新的事件触发时,这个定时器里的回调函数才会执行。
  4. this 的指向问题与 fn.call(that, args): 在事件监听器中,this 通常指向触发事件的DOM元素。然而,当我们把事件处理函数包装到 debounce 返回的函数中时,setTimeout 内部的回调函数中的 this 会指向全局对象(在浏览器中是 window),而不是我们期望的DOM元素。

    • var that = this;:为了解决 this 指向丢失的问题,我们在返回的函数内部,先用 that 变量保存了当前 this 的值(即触发事件的DOM元素)。
    • fn.call(that,args);:然后,在 setTimeout 的回调函数中,我们使用 fn.call(that, args) 来调用原始的 fn 函数。call 方法可以改变函数执行时的 this 指向,并传递参数。这样,ajax 函数在执行时,它的 this 就能正确地指向 inputb 元素了(尽管在这个 ajax 例子中 this 没用到,但这是一个良好的实践)。args 则是事件对象或其他参数。

inputb 的应用:防抖效果演示

现在,我们将 debounce 函数应用到 inputb 输入框上:

const inputb = document.getElementById('debounce');
let debounceAjax = debounce(ajax,500); // 将ajax函数防抖处理,延迟500毫秒
inputb.addEventListener('keyup',function(e) {
    debounceAjax(e.target.value); // 调用防抖后的函数
});

当你快速在 inputb 中输入内容时,你会发现 ajax requestconsole.log 消息不会像 inputa 那样频繁出现。只有当你停止输入500毫秒后,才会发送一次请求。这大大减少了不必要的请求,提升了性能和用户体验!👍

节流(Throttle):“稳住,我们能赢!”

节流是什么?

如果说防抖是“我只关心最后一次”,那么节流就是“稳住,我们能赢!”。它的核心思想是:在一定时间内,无论事件触发多少次,回调函数都只执行一次。

用一个形象的比喻来说,节流就像是“技能冷却时间”: 在游戏中,你释放了一个技能后,会有一个冷却时间。在这个冷却时间内,无论你再怎么按技能键,技能都不会再次释放。只有当冷却时间结束后,你才能再次释放技能。它控制的是执行的频率。

常见的应用场景

  • 滚动加载(Scroll Loading):当用户滚动页面时,我们可能需要判断是否滚动到底部,然后加载更多数据。如果每次滚动都触发加载逻辑,会非常耗费性能。使用节流可以确保在滚动过程中,每隔一段时间才检查一次是否需要加载。
  • 高频点击:防止用户在短时间内重复点击按钮,例如点赞、收藏等操作,避免多次提交。
  • 游戏射击:在射击游戏中,按住射击键会持续射击,但射击频率是固定的,而不是按键频率。

核心代码解析:throttle 函数

接下来,我们看看 throttle 函数的实现:

// 节流 fn 执行的任务 
function throttle(fn,delay) {
    let last,deferTime; // last记录上次执行时间,deferTime用于处理最后一次触发
    return function() {
        let that = this;// 保存当前函数的this上下文
        let _args = arguments;// 保存当前函数的参数(类数组对象)
        let now = + new Date();// 获取当前时间戳(毫秒数)

        // 第一次触发时,last为undefined,直接执行
        // 如果上次执行过,并且当前时间还没到上次执行时间 + delay
        if(last && now < last + delay) {
            clearTimeout(deferTime); // 清除上次设置的延迟定时器
            // 重新设置一个定时器,确保在delay时间后执行一次
            deferTime = setTimeout(function(){
                last = now; // 更新上次执行时间
                fn.apply(that,_args); // 执行原始函数
            }, delay);
        } else {
            // 如果是第一次触发,或者已经过了delay时间
            last = now; // 更新上次执行时间
            fn.apply(that,_args); // 立即执行原始函数
        }
    }
}

这段代码的逻辑比防抖稍微复杂一点,但理解了核心思想就很容易掌握:

  1. let last, deferTime; —— 时间戳与定时器的组合拳

    • last:用于记录上一次函数真正执行的时间戳。这是判断是否达到“冷却时间”的关键。
    • deferTime:这是一个定时器ID,它的作用是为了处理“最后一次触发”的情况。在某些节流实现中,如果事件停止触发,可能会导致最后一次事件没有被执行。deferTime 确保了即使事件停止,也能在 delay 时间后执行一次。
  2. let now = + new Date(); —— 获取当前时间戳+ new Date() 是一种简洁地将 Date 对象转换为时间戳(毫秒数)的方法。

  3. 核心判断逻辑

    • if(last && now < last + delay):这个条件判断是节流的关键。
      • last 存在:说明函数之前至少执行过一次。
      • now < last + delay:当前时间距离上一次执行的时间,还没有超过 delay 设定的间隔。
      • 如果这两个条件都满足,说明现在还在“冷却时间”内。此时,我们不立即执行 fn
        • clearTimeout(deferTime);:清除之前可能设置的延迟定时器。
        • deferTime = setTimeout(...):重新设置一个定时器。这个定时器的作用是,如果用户在 delay 时间内持续触发事件,那么每次触发都会清除上一个 deferTime 定时器并重新设置。这样,当用户停止触发事件后,最后一个 deferTime 定时器会在 delay 时间后执行一次 fn,确保了 “最后一次” 的执行。
    • else:如果 last 不存在(第一次触发),或者 now >= last + delay(已经过了冷却时间)。
      • last = now;:更新 last 为当前时间,表示函数已经执行。
      • fn.apply(that,_args);:立即执行原始函数 fn
  4. thisarguments 的传递: 与防抖函数类似,throttle 函数也需要正确处理 this 指向和参数传递。

    • let that = this;:保存当前 this 上下文。
    • let _args = arguments;arguments 是一个类数组对象,包含了函数被调用时传入的所有参数。我们将其保存下来,以便在 fn.apply(that, _args) 中传递给原始函数。

inputc 的应用:节流效果演示

现在,我们将 throttle 函数应用到 inputc 输入框上:

const inputc = document.getElementById('throttle');
let throttleAjax = throttle(ajax,500); // 将ajax函数节流处理,间隔500毫秒执行
inputc.addEventListener('keyup',function(e) {
    throttleAjax(e.target.value); // 调用节流后的函数
});

当你快速在 inputc 中输入内容时,你会发现 ajax requestconsole.log 消息会以大约每500毫秒一次的频率出现,而不是像 inputa 那样每次按键都触发,也不是像 inputb 那样只在停止输入后触发一次。它确保了在持续触发事件的情况下,函数也能以一个稳定的频率执行。

虽然这里我们用 keyup 事件来演示节流,但更典型的节流应用场景是滚动加载。想象一下淘宝、京东等电商网站的商品列表,当你向下滚动时,新的商品会不断加载出来。如果每次滚动都触发加载,那页面会卡得飞起。而使用节流,就可以控制每隔一段时间(比如200毫秒)才检查一次是否需要加载新数据,从而保证页面的流畅性。

防抖 vs 节流:如何选择?

现在你已经掌握了防抖和节流的实现原理,那么问题来了:在实际项目中,我该如何选择呢?🤔

别急,我们来总结一下它们的核心区别和适用场景:

特性防抖(Debounce)节流(Throttle)
核心思想只执行最后一次:事件触发后,等待一段时间再执行,期间再次触发则重新计时。间隔执行:在一定时间内,无论事件触发多少次,都只执行一次。
形象比喻电梯关门、射击游戏中的狙击枪(瞄准后一枪)技能冷却、射击游戏中的冲锋枪(持续射击但有射速限制)
适用场景用户停止操作后才执行,例如:
✅ 搜索框输入(停止输入后才搜索)
✅ 窗口resize(停止调整大小后才重新布局)
✅ 表单提交(防止重复提交)
持续操作中需要控制执行频率,例如:
✅ 滚动加载(每隔一段时间检查是否需要加载)
✅ 鼠标移动事件(控制移动轨迹的记录频率)
✅ 按钮高频点击(防止短时间内多次触发)

简单来说:

  • 如果你希望用户停止操作后才执行某个功能,那就用防抖。比如搜索建议,用户输入过程中不需要频繁请求,等他输完了再给建议。
  • 如果你希望在持续操作中,以固定的频率执行某个功能,那就用节流。比如滚动加载,用户一直在滚动,但我们不需要每滚动一像素就加载一次,而是每隔一段时间加载一次。

总结与展望:性能优化,永无止境

恭喜你!🎉 通过本文的学习,你已经掌握了前端性能优化中两大基石——防抖和节流的奥秘。我们从一个简单的“频繁触发”问题出发,逐步深入到防抖和节流的原理、高阶函数、闭包、this指向、定时器管理等核心知识点,并通过详细的代码解析,让你不仅知其然,更知其所以然。

现在,你可以在自己的项目中灵活运用这些技巧了:

  • 当遇到用户输入、窗口调整、表单提交等场景时,想想防抖,让你的应用更“稳重”。
  • 当遇到页面滚动、鼠标移动、按钮高频点击等场景时,想想节流,让你的应用更“流畅”。

前端性能优化是一个永无止境的话题,防抖和节流只是其中的冰山一角。但它们却是最常用、最有效的优化手段之一。掌握了它们,你就迈出了成为前端性能优化高手的第一步!

希望这篇轻松欢快又干货满满的文章,能帮助你在编程的道路上越走越远,写出更多高性能、用户体验棒棒的应用!加油,编程爱好者们!🚀✨