JavaScript 实现 Underscore 防抖(debounce)函数详解

作者:代码拾荒时间:2026-10-25 20:34:04浏览:638

本文详细讲解如何从零实现 Underscore 库中的防抖(debounce)函数。通过处理 resize、scroll 等高频事件,解决回调执行频繁导致的性能卡顿。文章逐步演示了如何处理 this 指向、event 对象传递、返回值返回,以及立即执行(immediate)和取消(cancel)功能的完整实现过程。

JavaScript 专题系列首篇,深入解析防抖机制,并引导读者从零开始构建一个符合 Underscore 规范的 debounce 函数。

前言

在前端开发场景中,常需处理高频触发的事件,例如:

  • 窗口 resize 与 scroll 事件
  • 鼠标 mousedown 与 mousemove 事件
  • 键盘 keyup 与 keydown 事件 ……

以下示例代码用于演示事件触发的高频特性:

创建一个 index.html 文件:

<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1">
    <title>debounce</title>
    <style>
        #container{
            width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
        }
    </style>
</head>
<body>
    <div id="container"></div>
    <script src="debounce.js"></script>
</body>
</html>

debounce.js 文件的具体代码如下:

var count = 1;
var container = document.getElementById('container');
function getUserAction() {
    container.innerHTML = count++;
};
container.onmousemove = getUserAction;

观察运行效果:

前言

debounce

鼠标从左侧滑至右侧,触发 getUserAction 函数高达 165 次!

鉴于此示例极为简单,浏览器能够轻松应对。然而,若涉及复杂的回调函数或 AJAX 请求呢?假设在 1 秒内触发了 60 次,每个回调函数必须在 1000 / 60 = 16.67ms 内完成执行,否则便会出现卡顿现象。

针对此问题,通常采用两种解决方案:

  • debounce 防抖
  • throttle 节流

防抖

本文重点阐述防抖功能的实现细节。

防抖的核心逻辑为:无论事件触发频率多高,均确保在事件触发 n 秒后执行。若在 n 秒内再次触发,则重置计时器,以最新触发时间为准重新计算 n 秒后执行。简言之,必须等待事件停止触发 n 秒后,才执行回调,策略十分严格。

第一版

基于上述描述,编写第一版代码:

// 第一版
function debounce(func, wait) {
    var timeout;
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait);
    }
}

若使用初始示例场景:

container.onmousemove = debounce(getUserAction, 1000);

此时无论鼠标如何移动,只要停止操作 1000ms 内无新触发,事件即被执行。

执行次数从 165 次骤降至 1 次!

效果显著,继续完善功能。

this

若在 getUserAction 函数中执行 console.log(this),在未使用 debounce 函数时,this 的值为:

<div id="container"></div>

然而,若使用我们实现的 debounce 函数,this 将错误指向 Window 对象!

因此,需修正 this 的指向。

修改代码如下:

// 第二版
function debounce(func, wait) {
    var timeout;
    return function () {
        var context = this;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context)
        }, wait);
    }
}

此时 this 指向已修正。接着探讨下一个问题:

event 对象

JavaScript 事件处理函数会接收 event 对象,修改 getUserAction 函数如下:

function getUserAction(e) {
    console.log(e);
    container.innerHTML = count++;
};

若不使用 debounce 函数,此处将打印 MouseEvent 对象,如图所示:

前言

MouseEvent

但在我们实现的 debounce 函数中,却仅打印 undefined!

故再次修改代码:

// 第三版
function debounce(func, wait) {
    var timeout;
    return function () {
        var context = this;
        var args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

返回值

需注意,getUserAction 函数可能包含返回值,因此需返回函数的执行结果。

// 第四版
function debounce(func, wait) {
    var timeout, result;
    return function () {
        var context = this;
        var args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            result = func.apply(context, args)
        }, wait);
        return result;
    }
}

至此,修复了三个关键问题:

  • this 指向
  • event 对象
  • 返回值

立刻执行

当前代码已较完善,但为满足更高需求,思考新场景。

该需求描述为:

不希望强制等待事件停止触发后才执行,而是希望立即执行函数,随后等待 n 秒后,方可重新触发执行。

此需求合理,故添加 immediate 参数以判断是否立即执行。

// 第五版
function debounce(func, wait, immediate) {
    var timeout, result;
    return function () {
        var context = this;
        var args = arguments;
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                result = func.apply(context, args)
            }, wait);
        }
        return result;
    }
}

取消

最后考虑一个小需求:希望能取消 debounce 函数。例如,若 debounce 间隔为 10 秒且 immediate 为 true,则需等待 10 秒后方可重新触发。现希望提供一个按钮,点击后取消防抖,以便再次触发时可立即执行,是否令人欣喜?

为满足此需求,编写最后一版代码:

// 第六版
function debounce(func, wait, immediate) {
    var timeout, result;
    var debounced = function () {
        var context = this;
        var args = arguments;
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) result = func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                result = func.apply(context, args)
            }, wait);
        }
        return result;
    };
    debounced.cancel = function() {
        clearTimeout(timeout);
        timeout = null;
    };
    return debounced;
}

演示效果如下:

前言

debounce-cancel

至此,已完整实现 Underscore 中的 debounce 函数,恭喜成功!

专题系列

JavaScript 专题系列:https://www.jb51.net/list/list_3_1.htm

JavaScript 专题系列计划撰写二十余篇,主要研究日常开发中功能点的实现,如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) Underscore 和 jQuery 的实现方式。

以上为 JavaScript 专题之 Underscore 防抖实例学习的详细内容,更多关于 JavaScript Underscore 防抖的资料请关注本站其它相关文章!

作者最新文章