手搓可选择在防抖前或后执行事件处理的高级版防抖

1,196 阅读6分钟

防抖

基础篇传送门--->几分钟学会手搓防抖

可以通过这个传送门了解一些防抖的知识,然后开始接下来的进阶操作。

案例介绍

在了解防抖的基本原理后,通过一个案例详细描述延后执行事件的防抖、立即执行事件的防抖以及,可选择执行的先后的防抖的实现。

image.png

触发事件和事件处理:当鼠标点击在灰色阴影处使白色数字会进行累加。

  • 实现延后执行事件处理的防抖:当用户一直点击灰色区域,白色数字不会进行累加,当用户每点击一次就会设置一个计时器,并且覆盖之前的计时器,当用户的最后一次点击时,计时器被设置开始计时,到一定时间后,白色数字才会进行累加。

  • 实现一种可立即执行事件处理的防抖机制:当用户连续点击灰色区域时,确保首次点击会即刻触发白色数字的递增操作,这是在计时器为null或undefined的情况下发生的。伴随首次点击,计时器随即启动。在用户连续快速点击的过程中,每一次新的点击都会重置这一计时器,使其重新开始倒数延迟,确保了在连续点击期间,不会发生白色数字的递增操作。直至用户停止点击,最后一次点击后,计时器在经过一定的延时时间后会将null赋值给计时器,让下一次点击可以立即触发白色数字的递增操作。

  • 实现可选择事件处理是立即执行还是延后执行的防抖。

案例共同的css部分和html部分

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</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>
</body>
</html>

案例共同的JavaScript部分

获取id为container的元素,设置一个计数器和一个事件处理函数getUserAction

事件处理函数getUserAction实现将计数器累加后将id为container的元素的内容更新为该计数器的值。也就是说,每次调用这个函数,都会使 container 中显示的数字增加 1。

事件处理函数的参数是事件对象。

var count = 1;
var container = document.getElementById('container');

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

延后执行处理事件的防抖

简单版(复习一下防抖)

设计一个简单的防抖函数:

该防抖函数有2个参数,一个是事件处理函数,一个是计时器延时时间。

  1. 在防抖函数中声明一个timer变量并赋值为null。
  2. 返回一个匿名函数,匿名函数实现清除计时器和设置计时器的功能:
    1. 在匿名函数中通过执行clearInterval(timer);将null设置为null;
    2. timer设置为一个计时器,在一定延时时间内执行事件处理函数。
var count = 1;
var container = document.getElementById('container');

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

function debounce(fn, delay) {
    let timer = null;
    return function () {
        clearInterval(timer);
        timer = setTimeout(fn, delay)
    }
}

将防抖函数的返回函数与点击事件绑定在一起,并且设置计时器的延时时间。

container.onclick = debounce(getUserAction, 500);

你是否会有疑问,为什么返回的匿名函数可以访问防抖函数内部定义的timer。是因为涉及闭包的知识。有疑问才会有进步。在了解完闭包后再阅读下文效果更佳。

缺点

这样的代码十分便捷,但是有很大的缺点。相当于是丢了西瓜捡了芝麻。

这样的防抖函数干了什么呢?

  • 将事件处理函数的this指向改为了window,不应该是指向container元素才对。
  • 将事件处理函数的事件对象搞丢了。

改进版

首先将this指向修改回来。

因为防抖函数的返回函数与点击事件绑定在一起,所以匿名函数的this的指向是container元素,所以可以借用匿名函数的this指向,将事件处理函数的this指向进行显式绑定修改回来。

要怎么借用匿名函数的this呢?

可以通过变量保存this实现。

function debounce(fn, delay) {
    let timer = null;
    return function () {
        var that = this;
        clearInterval(timer);
        timer = setTimeout(function () {
            fn.apply(that);
        }, delay)
    }
}

其次将事件对象找回来。

因为防抖函数的返回函数与点击事件绑定在一起,所以匿名函数有事件对象。

我们可以同样借用变量保存进行借用,然后通过传参的方法传递回事件处理函数。

function debounce(fn, delay) {
    let timer = null;
    return function () {
        var that = this;
        var args = arguments;
        clearInterval(timer);
        timer = setTimeout(function () {
            fn.apply(that, args);
        }, delay)
    }
}

修改版代码:

var count = 1;
var container = document.getElementById('container');

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

function debounce(fn, delay) {
    let timer = null;
    return function () {
        var that = this;
        var args = arguments;
        clearInterval(timer);
        timer = setTimeout(function () {
            fn.apply(that, args);
        }, delay)
    }
}

container.onclick = debounce(getUserAction, 500);

实现效果

通过该防抖函数实现了在匿名函数执行后清除上一个计时器后设置新的计时器,在新的计时器计时结束后执行事件处理函数。这样的执行逻辑就实现了延后效果。

立即执行事件的防抖

思路

立即执行事件的防抖函数的主体思路:

  1. 首先判断是否存在计时器,如果存在就清除掉。
  2. timer 存储的是计时器的ID是数值但其值不会是0或NaN,通过!timer转化为布尔值判断计时器的状态是否为null或undefined。
    1. 如果timer是计时器的ID,则!timer为false;
    2. 如果timer是null或undefined,则!timer为true;
  3. 然后设置一个计时器,在延时一定时间后将timer设置为null。
  4. timer为null,就执行事件处理函数。

不给timer初始值会默认为undefined,然后当点击事件发生就会立即执行事件处理函数,并且设置了一个计时器恢复计时器为null的状态。直到第一次点击后,下一次执行事件处理函数的时候是计时器没有被重置而是计时结束恢复计时器为null的状态的时候。

function debounce(fn, delay) {
    let timer;
    var result;
    return function () {
        var args = arguments;
        var that = this;
        if (timer) clearTimeout(timer);
        var callNow = !timer;
        timer = setTimeout(function () {
            timer = null;
        }, delay)
        if (callNow) result = fn.apply(that, args);
        return result;
    }
}
container.onclick = debounce(getUserAction, 500);

这和上一个防抖函数的区别在哪呢?听我细细道来:

  1. 进行了优化,用一个result变量保存事件处理函数的返回结果。这个优化也可以实现在上一个防抖函数上。
  2. 重要的部分来了:该防抖函数并不是将事件处理函数放在计时器的回调函数上等待计时结束后执行,而是放在计时器的外面,通过重置timer为null实现事件处理函数的执行。

可选择执行事件先后的防抖

对之前两种防抖函数进行了整合,给防抖函数添加了一个immediate参数,当值为true时,选择立即执行事件处理,当值为false时,选择延后执行事件处理。

function debounce(fn, delay, immediate) {
    let timer;
    var result;

    return function () {

        var args = arguments;
        var that = this;

        if (timer) clearTimeout(timer);

        if (immediate) {
            var callNow = !timer;
            timer = setTimeout(function () {
                timer = null;
            }, delay)
            if (callNow) result = fn.apply(that, args);
        } else {
            timer = setTimeout(function () {
                result = fn.apply(that, args);
            }, delay)
        }
        return result;
    }
}
container.onclick = debounce(getUserAction, 500, true);