告别页面卡顿:生动解析 JavaScript 防抖与节流

2 阅读5分钟

告别页面卡顿:生动解析 JavaScript 防抖与节流

在前端开发的“战场”上,我们常常会遇到一些“话痨”事件:用户疯狂点击按钮、快速输入搜索词、或者像拉面条一样不停拖动窗口大小。如果对这些高频事件来者不拒,浏览器很快就会因为处理不过来而“罢工”,导致页面卡顿甚至崩溃。

为了解决这个问题,我们需要两位“交通指挥官”:防抖(Debounce)节流(Throttle)

今天,我们就结合一段经典的实战代码,来看看这两位指挥官是如何维持秩序,让页面性能稳如泰山的。


️ 核心代码:两位指挥官的“真身”

首先,让我们直面你提供的这段核心代码。这不仅仅是几行 JavaScript,这是控制事件频率的“宪法”。

我们将以这个通用的 ajax 请求函数作为被管理的对象:

function ajax(content) {
    console.log('ajax request', content);
}

️ 防抖: “等你想好了再告诉我”

防抖(Debounce)的核心逻辑是:“不管你怎么触发,我只在最后一次操作结束后的 N 毫秒执行。”

这就好比电梯关门:电梯门打开后,只要还有人陆续进来(触发事件),门就会一直开着,计时器重置。只有当一段时间(比如 5 秒)没人进出了,门才会真正关上(执行函数)。

让我们看看代码是如何实现这一逻辑的:

function debounce(fn, delay) {
    var id; // 闭包中的自由变量,用于存储定时器ID
    return function(args) {
        if(id) clearTimeout(id); // 核心:如果之前有定时器,立马清除(重置计时)
        var that = this;
        
        id = setTimeout(function() {
            fn.call(that, args) // 延迟执行真正的函数
        }, delay);
    }
}
  • 闭包的妙用var id 被包裹在闭包中,这意味着每次触发事件时,我们都能访问到同一个定时器变量。
  • 重置机制clearTimeout(id) 是防抖的灵魂。用户在输入框里每敲一个键,之前的计时就被打断,重新开始倒数。
  • this 的指向:代码中特意保存了 var that = this,并在 setTimeout 中使用 fn.call(that, args)。这是为了防止定时器执行时 this 意外指向 window,确保上下文环境正确。

节流: “不管多急,请按排队顺序来”

节流(Throttle)的核心逻辑是:“不管你怎么触发,我每隔 N 毫秒只执行一次。”

这就像水龙头滴水:无论你水龙头拧得有多快多猛,水滴只能按照固定的频率一滴一滴往下落。或者想象一下机枪射击,扣住扳机不放,子弹也是按射速一颗颗射出,而不是一瞬间把弹夹全打光。

代码实现稍微复杂一点,它结合了“时间戳”和“定时器”的双重保险(混合版节流):

function throttle(fn, delay) {
    let last, // 记录上次执行的时间戳
        deferTimer; // 定时器
    return function(args) {
        let that = this; 
        let _args = arguments; 
        let now = + new Date(); // 获取当前时间戳
        
        // 如果上次执行过,且当前时间还没到下次执行的时间点(在冷却期内)
        if(last && now < last + delay) {
            clearTimeout(deferTimer);
            // 设置一个定时器,确保在冷却期结束后至少执行一次(这是混合版的优势)
            deferTimer = setTimeout(function() {
                last = now;
                fn.apply(that, _args);
            }, delay);

        } else { 
            // 第一次触发,或者已经过了冷却期,立即执行
            last = now;
            fn.apply(that, _args);
        }
    }        
}
  • 时间戳判断now < last + delay 用来判断是否处于“冷却时间”内。
  • 混合策略:这段代码非常精妙。如果在冷却期内,它会设置一个 deferTimer。这意味着,如果用户一直触发事件,函数不仅会立即执行一次(else 分支),还会在停止触发后的 delay 时间后再执行一次(if 分支里的 setTimeout)。这保证了操作的开头结尾都不会被遗漏。

️ 实战演练:三个输入框的较量

为了直观地展示效果,代码中设置了三个输入框,分别对应“无限制”、“防抖”和“节流”三种状态:

const inputa = document.getElementById('undebounce'); // 裸奔的输入框
const inputb = document.getElementById('debounce');   // 穿了防抖铠甲
const inputc = document.getElementById('throttle');   // 装了节流阀门

let debounceAjax = debounce(ajax, 500); // 500ms 防抖
let throttleAjax = throttle(ajax, 1000); // 1000ms 节流

// 1. 无限制:用户每敲一个字,控制台就打印一次。如果敲得快,请求会堆积。
inputa.addEventListener('keyup', function(e) {
    ajax(e.target.value) 
})

// 2. 防抖:用户快速输入 "Hello",控制台只会打印一次 "Hello"。
// 只有当用户停下手超过 500ms,请求才会发送。
inputb.addEventListener('keyup', function(e) {
    debounceAjax(e.target.value) 
})

// 3. 节流:用户快速输入。
// 第一次按键立即打印。
// 接下来 1 秒内的按键会被忽略,或者在停止 1 秒后打印最后一次。
inputc.addEventListener('keyup', function(e) {
    throttleAjax(e.target.value);
})

总结:何时请哪位指挥官?

特性防抖节流
核心逻辑最后一次说了算固定频率执行
生活比喻电梯关门、核弹发射按钮水龙头滴水、机关枪射击
适用场景搜索框输入(等用户输完再搜)、窗口 resize(等拖完再计算布局)、表单验证滚动加载(scroll 事件,每隔一段距离加载一次)、按钮点击(防止重复提交)、鼠标移动(mousemove)
代码特征clearTimeout 是核心Date.now()setTimeout 周期性执行

一句话口诀: 如果**“等用户停下来再做”,请用防抖**; 如果**“不管用户多快,我要按节奏来”,请用节流**。

掌握这两段代码,你就掌握了前端性能优化的半壁江山! 这篇解析是否清晰地展示了防抖与节流的区别?如果需要,我可以为你补充这两个函数的定时器版节流实现,或者整理一份面试中常见的防抖节流考点,你需要吗?