深入探讨防抖函数

随着前端技术的不断发展,构建高性能、流畅用户体验的网页应用变得越来越重要。在日常开发中,我们经常会遇到需要处理用户输入、滚动事件、窗口调整大小等高频触发的操作。但是,如果不采取适当的措施,这些事件可能会引发性能问题,影响用户体验。

本文将带您深入探讨防抖函数,一个在前端开发中广泛应用的工具,以及它是如何帮助我们提升性能和用户体验的。让我们一起来了解它的原理、用途和实际应用。

1. 什么是防抖函数?

防抖函数是一种前端开发中常用的技巧,用于限制某个函数在短时间内多次触发时的执行次数。它确保在指定的等待时间内,只有最后一次触发函数的操作会被执行,而之前的操作都会被忽略。这在处理用户输入、窗口调整大小和滚动等高频事件时非常有用。

2. 问题:为什么需要防抖?

在我们深入了解防抖函数之前,让我们首先思考一个问题:为什么需要防抖?

2.1. 高频事件会引发性能问题

当用户在输入框中快速输入文字时,每次按键都会触发输入事件。如果我们不加以控制,可能会导致大量的事件处理函数执行,对性能产生负面影响。

2.2. 重复操作可能会带来不一致性

在某些情况下,用户可能会多次点击某个按钮或触发某个操作,但我们希望只处理最后一次操作,以避免不一致性或冲突。

2.3. 减少不必要的资源浪费

执行不必要的操作不仅浪费了计算资源,还可能导致不必要的网络请求,对服务器造成额外压力。因此,我们需要一种方法来合理控制事件的触发和处理。

3. 防抖函数的工作原理

我们举个示例代码来了解事件如何频繁的触发。

我们写个 index.html 文件:

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 文件的代码如下:

js 复制代码
var count = 1;
var container = document.getElementById('container');

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

container.onmousemove = getUserAction;

从左边滑到右边就触发了 165 次 getUserAction 函数!

因为这个例子很简单,所以浏览器完全反应的过来,可是如果是复杂的回调函数或是 ajax 请求呢?假设 1 秒触发了 60 次,每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会有卡顿出现。

知识点:浏览器的执行间隔为什么是16.7ms?

为了解决这个问题,一般有两种解决方案:

  1. debounce 防抖
  2. throttle 节流

防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行。

3.1. 延时

根据这段表述,我们可以写第一版的代码:

js 复制代码
function debounce(func, wait) {
    var timeout;
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait);
    }
}

知识点:为什么debounce函数内部返回一个闭包?

如果我们要使用它,以最一开始的例子为例:

js 复制代码
container.onmousemove = debounce(getUserAction, 1000);

现在随你怎么移动,反正你移动完 1000ms 内不再触发,我才执行事件。

3.2. this指向

如果我们在 getUserAction 函数中 console.log(this),在不使用 debounce 函数的时候,this 的值为:

html 复制代码
<div id="container"></div>

但是如果使用我们的 debounce 函数,this 就会指向 Window 对象!因为它是在setTimeout里面的。

所以我们需要将 this 指向正确的对象。

我们修改下代码:

js 复制代码
function debounce(func, wait) {
    var timeout;

    return function () {
        var context = this;

        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context)
        }, wait);
    }
}

现在 this 已经可以正确指向了。让我们看下个问题。

3.3. event对象

JavaScript 在事件处理函数中会提供事件对象 event,我们修改下 getUserAction 函数:

js 复制代码
function getUserAction(e) {
    console.log(e);
    container.innerHTML = count++;
};

如果我们不使用 debouce 函数,这里会打印 MouseEvent 对象,如图所示:

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

所以我们再修改一下代码:

js 复制代码
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中就能正确的拿到event参数对象了。

如果我们想给事件绑定参与,也可以这样去写了:

js 复制代码
function getUserAction(a) {
  console.log(a)
  container.innerHTML = count++;
};

...
container.onmousemove = debounce(getUserAction.bind(null, 1), 10000);

3.4. 立刻执行

我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行事件。

想想这个需求也是很有道理的嘛,那我们加个 immediate 参数判断是否是立刻执行,如果需要立刻执行,那我们在立刻触发事件绑定的函数以后,在这触发之后的n秒后,才能再次出发事件绑定的函数。下面我们实现一下:

js 复制代码
function debounce(func, wait, immediate) {

    var timeout;

    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) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

我们通过immediate参数来判断是否需要立即执行。

3.5. 返回值

此时注意一点,就是 getUserAction 函数可能是有返回值的,所以我们也要返回函数的执行结果,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候返回函数的执行结果。

js 复制代码
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;
    }
}

3.6. 取消

最后我们再思考一个小需求,我希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦。下面我们来实现一下:

js 复制代码
// 完整版
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;
}

那么该如何使用这个 cancel 函数呢?依然是以上面的 demo 为例:

js 复制代码
var count = 1;
var container = document.getElementById('container');

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

var setUseAction = debounce(getUserAction, 10000, true);

container.onmousemove = setUseAction;

document.getElementById("button").addEventListener('click', function(){
    setUseAction.cancel();
})

至此我们已经完整实现了一个 underscore中的 debounce 函数了!

4. 总结

防抖函数是前端开发中非常有用的工具,可以帮助我们控制高频触发的事件,提高性能和用户体验。通过合理设置等待时间,我们可以在保证功能完整性的前提下,减少不必要的资源浪费和操作的执行次数。

在实际项目中,根据具体需求和场景,可以选择使用现有的防抖函数库或自行实现一个。无论哪种方式,防抖函数都是优化前端代码的有力工具,值得我们深入学习和应用。希望本文能够帮助您更好地理解和运用防抖函数,提升您的前端开发技能。

相关推荐
y先森8 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy8 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891111 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端