js面试(防抖)

一、什么是防抖

防抖(Debounce)是一种用于减少特定事件触发频率的技术。在编程中,它通常用于确保函数或方法不会在很短的时间内被频繁调用,这有助于优化性能并避免不必要的计算或操作。

防抖的实现原理是,在事件被触发后,一个定时器会被设置。如果在定时器完成之前,相同的事件再次被触发,那么原来的定时器会被取消,并重新设置一个新的定时器。这样,只有在最后一次事件触发后的一定时间内没有再次触发,定时器才会执行其回调函数。

应用场景:

  1. 登录与发送短信:在连续点击登录按钮或发送短信时,防抖技术能够确保不会因用户点击过快而发送多次请求。
  2. 表单验证:在用户输入表单信息时,防抖技术可以确保不会因为频繁触发验证逻辑而导致性能降低。
  3. 实时搜索与保存:在文本编辑器或搜索框中实现实时搜索和保存功能时,防抖技术可以确保在用户停止输入一段时间后执行搜索或保存操作,避免用户连续输入导致的频繁触发。
  4. 窗口大小调整:在调整浏览器窗口大小时,resize事件可能会被频繁触发,防抖技术可以确保只执行一次操作,避免不必要的计算。
  5. 鼠标移动事件:实现一些需要用户停止移动鼠标后再执行的功能时,如拖拽功能,防抖技术可以减少事件的处理频率。

二、前置准备

  1. 准备一个html文件和一个debounce.js文件,debounce.js文件用来编写防抖函数

    <!-- test.html -->
    <!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 class="debounce">触发防抖事件</div>
    <script src="./debounce.js"></script>
    </body>
    </html>
    // debounce.js
    const debounce = () => {};
  2. div绑定点击事件

    <!-- test.html -->
    <!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 class="debounce">触发防抖事件</div>
    <script src="./debounce.js"></script>
    <script>
    const clickEvent = (e) => { console.log("点击事件触发", e, this) }
    document.querySelector(".debounce").addEventListener("click", clickEvent)
    </script>
    </body>
    </html>

    image-20240317023413975

    进行点击测试,发现目前this指向的是Window,因为箭头函数没有this,通过作用域链往外找,就找到Window了。

    将箭头函数改为普通函数,就能将this指向改为div。但是这里暂时先不改,后面遇到问题再说。

  3. 将clickEvent方法传递给debounce进行处理

    <!-- test.html -->
    <!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 class="debounce">触发防抖事件</div>
    <script src="./debounce.js"></script>
    <script>
    const clickEvent = (e) => { console.log("点击事件触发", e, this) }
    const debounceClickEvent = debounce(clickEvent, 2000)
    document.querySelector(".debounce").addEventListener("click", debounceClickEvent)
    </script>
    </body>
    </html>

    现在再进行点击测试,发现控制台什么也没有输出。因为目前debounce方法还没写方法体,没有返回值,所以debounceClickEvent是undefined,所以什么也不会触发。

    下面我们就一步步写防抖函数

三、基础防抖实现

  1. 思考防抖函数需要接收什么参数,又需要返回什么

    • 接收参数:一个点击事件的处理方法、延迟执行的时间
    • 返回值:做了防抖处理的方法

    根据以上条件,可以先写出如下代码

    // debounce.js
    const debounce = (fun, time) => {
    return fun;
    };

    上面代码接收了一个方法,又直接将该方法返回了。等于什么也没做,至少也得做个延时处理吧

    // debounce.js
    const debounce = (fun, time) => {
    return () => {
    setTimeout(fun, time);
    };
    };

    上面的方法虽然做了延时处理,但还是造成了处理方法被多次调用

    那我们应该怎样让前面的处理方法被取消执行呢?既然处理方法都放进了定时器,那我们就把定时器清除就行了。

    // debounce.js
    const debounce = (fun, time) => {
    let timer;
    return () => {
    timer && clearTimeout(timer);
    timer = setTimeout(fun, time);
    };
    };

    image-20240317025923041

    这下多次点击,就只触发了最后一次处理方法了。但是通过打印可以发现,this指向的是Window,事件源也是undefined。

    我们如何将this指向变成调用方法的div,又如何获取事件源呢

  2. 改变this指向

    我们先分析一下,为什么this指向的是window?

    const clickEvent = (e) => { console.log("点击事件触发", e, this) }
    const debounceClickEvent = debounce(clickEvent, 2000)
    // debounce.js
    const debounce = (fun, time) => {
    let timer;
    return () => {
    timer && clearTimeout(timer);
    timer = setTimeout(fun, time);
    };
    };

    上面的写法,其实等价于下面的写法

    // debounce.js
    const debounce = (fun, time) => {
    let timer;
    return () => {
    timer && clearTimeout(timer);
    timer = setTimeout((e) => { console.log("点击事件触发", e, this) }, time);
    };
    };

    那再来分析一下this指向,箭头函数没有this,它通过作用域链往外找,就找到Window了

    那我们在哪里能获取到正确的this指向呢?来看看哪个方法是被div调用的,是不是return 后面那个方法

    既然这个方法是div调用的,那我们应该可以拿到this,但是这里也是箭头函数,没有this。所以我们先将它修改为普通函数

    // debounce.js
    const debounce = (fun, time) => {
    let timer;
    return function () {
    console.log(this);
    timer && clearTimeout(timer);
    timer = setTimeout(fun, time);
    };
    };

    我们打印一下this,看看是不是div

    image-20240317031539559

    这里的this指向确实是指向div的,那我们就可以通过call、apply、bind等方法修改fun的this指向了

    // debounce.js
    const debounce = (fun, time) => {
    let timer;
    return function () {
    timer && clearTimeout(timer);
    timer = setTimeout(fun.bind(this), time);
    };
    };

    再来看看能打印出正确的this吗

    image-20240317031756029

    结果this还是指向的Window,别忘了fun是一个箭头函数,它没有this,又怎么能去修改呢。

    所以我们需要将这个fun(clickEvent)也改为普通函数

    const clickEvent = function (e) {
    console.log("点击事件触发", e, this)
    }
    const debounceClickEvent = debounce(clickEvent, 2000)
    document.querySelector(".debounce").addEventListener("click", debounceClickEvent)

    这下再来看看this指向正确了吗

    image-20240317032129969

  3. 获取事件源

    为什么这里的e打印出来是undefined?

    我们来看看div调用的是哪个方法,是不是debounce函数中return的那个方法。那这个方法应该是可以接收到事件源的。

    再看看fun,我们使用bind的时候,根本就没有给它设置参数,所以e打印出来是undefined

    // debounce.js
    const debounce = (fun, time) => {
    let timer;
    return function (e) {
    console.log(e);
    timer && clearTimeout(timer);
    timer = setTimeout(fun.bind(this), time);
    };
    };

    image-20240317032714448

    从返回的方法里面确实可以拿到事件源,那我们将这个 e 传递给bind 的第二个参数就好了

    // debounce.js
    const debounce = (fun, time) => {
    let timer;
    return function (e) {
    timer && clearTimeout(timer);
    timer = setTimeout(fun.bind(this, e), time);
    };
    };

    image-20240317032857839

四、接收多个参数

  1. 上面已经实现了最基本的防抖函数,但还有一些地方需要优化

    • 如果传递了多个参数又如何接收
    • 每次触发新的点击事件,会清空定时器。那最后一次执行完了,又怎么清空呢?这个对象始终没有释放掉
  2. 接收多个参数

    // debounce.js
    const debounce = (fun, time) => {
    let timer;
    return function (...args) {
    timer && clearTimeout(timer);
    timer = setTimeout(fun.bind(this, ...args), time);
    };
    };
  3. 最后一次执行完毕,将timer释放掉

    // debounce.js
    const debounce = (fun, time) => {
    let timer;
    return function (...args) {
    timer && clearTimeout(timer);
    timer = setTimeout(() => {
    fun.apply(this, args);
    timer = null;
    }, time);
    };
    };

五、取消处理方法

  1. 怎么将最后一次的处理方法给取消呢?

    想办法拿到定时器timer,然后使用clearTimeout(timer)就可以了

  2. 准备一个div,作为取消按钮

    <!-- test.html -->
    <!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 class="debounce">触发防抖事件</div>
    <div class="cancle">取消</div>
    <script src="./debounce.js"></script>
    <script>
    const clickEvent = function (e) {
    console.log("点击事件触发", e, this)
    }
    const debounceClickEvent = debounce(clickEvent, 2000)
    const cancle = () => { }
    document.querySelector(".debounce").addEventListener("click", debounceClickEvent)
    document.querySelector(".cancle").addEventListener("click", cancle)
    </script>
    </body>
    </html>
  3. 在返回的函数身上再绑定一个方法用来清除定时器

    // debounce.js
    const debounce = (fun, time) => {
    let timer;
    const debounceEvent = function debounceEvent(...args) {
    timer && clearTimeout(timer);
    timer = setTimeout(() => {
    fun.apply(this, args);
    timer = null;
    }, time);
    };
    debounceEvent.cancle = () => {
    timer && clearTimeout(timer);
    timer = null;
    };
    return debounceEvent;
    };

    image-20240317035101187

  4. 给取消按钮绑定方法

    <!-- test.html -->
    <!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 class="debounce">触发防抖事件</div>
    <div class="cancle">取消</div>
    <script src="./debounce.js"></script>
    <script>
    const clickEvent = function (e) {
    console.log("点击事件触发", e, this)
    }
    const debounceClickEvent = debounce(clickEvent, 2000)
    const cancle = () => { debounceClickEvent.cancle() }
    document.querySelector(".debounce").addEventListener("click", debounceClickEvent)
    document.querySelector(".cancle").addEventListener("click", cancle)
    </script>
    </body>
    </html>

六、立即执行

  1. 如何让第一次处理函数立即执行,后面再做防抖处理

    可以通过一个变量来控制,第一次立即执行,然后将该变量取反,后面执行的时候,使用原来的逻辑

  2. 为debounce函数添加参数,用来标志是否第一次立即执行

    // debounce.js
    const debounce = (fun, time, immediately = false) => {
    let timer;
    const debounceEvent = function debounceEvent(...args) {
    timer && clearTimeout(timer);
    timer = setTimeout(() => {
    fun.apply(this, args);
    timer = null;
    }, time);
    };
    debounceEvent.cancle = () => {
    console.log("清除定时器");
    timer && clearTimeout(timer);
    timer = null;
    };
    return debounceEvent;
    };
  3. 修改第一次执行的逻辑

    // debounce.js
    const debounce = (fun, time, immediately = false) => {
    let timer;
    // 是否已经立即执行
    let running = false;
    const debounceEvent = function debounceEvent(...args) {
    timer && clearTimeout(timer);
    // 开启了立即执行并且还没有立即执行,则说明这是第一次,直接立即执行
    if (immediately && !running) {
    // 第一次已经立即执行了,后面再次触发就不能再立即执行了
    running = true;
    fun.apply(this, args);
    } else {
    timer = setTimeout(() => {
    fun.apply(this, args);
    timer = null;
    // 最后一次防抖方法完成后,下一次还可以立即执行
    running = false;
    }, time);
    }
    };
    debounceEvent.cancle = () => {
    console.log("清除定时器");
    timer && clearTimeout(timer);
    timer = null;
    // 取消最后一次防抖方法后,恢复下一次的立即执行
    running = false;
    };
    return debounceEvent;
    };

    上面的代码实现了第一次触发时立即执行,然后每次触发做防抖处理。最后一次防抖方法处理完成(或被取消)后,在下一次触发时,又可以立即执行。

    但是这仍然存在一个小问题,如果第一次立即执行后不触发频繁的点击操作,而是等第一次完成之后,再点击,这时还会立即执行吗?很明显不能。目前第一次立即执行后,想要恢复立即执行,就必须经过频繁触发事件,让最后一次防抖方法被处理了,才能再次恢复立即执行。

    在下面的代码中,我们使用了一个定时器。在第一次立即执行完成后,开启一个定时器,在一段时间后恢复立即执行,使再次点击时,可以立即执行。但是,如果在这一段时间内,频繁的触发了点击事件,那就清除定时器,在最后一次防抖处理方法完成后,再恢复立即执行。

    // debounce.js
    const debounce = (fun, time, immediately = false) => {
    let timer;
    // 是否已经立即执行
    let running = false;
    // 恢复立即执行的定时器
    let timerRunning;
    const debounceEvent = function debounceEvent(...args) {
    timer && clearTimeout(timer);
    // 开启了立即执行并且还没有立即执行,则说明这是第一次,直接立即执行
    if (immediately && !running) {
    // 第一次已经立即执行了,后面再次触发就不能再立即执行了
    running = true;
    fun.apply(this, args);
    // 第一次立即执行已经完成了,我们在一段时间后恢复立即执行
    timerRunning = setTimeout(() => {
    running = false;
    }, time);
    } else {
    // 如果频繁触发了事件,则不恢复立即执行,而是等最后一次处理方法完成再恢复立即执行
    timerRunning && clearTimeout(timerRunning);
    timer = setTimeout(() => {
    fun.apply(this, args);
    timer = null;
    // 最后一次防抖方法完成后,下一次还可以立即执行
    running = false;
    }, time);
    }
    };
    debounceEvent.cancle = () => {
    console.log("清除定时器");
    timer && clearTimeout(timer);
    timer = null;
    // 取消最后一次防抖方法后,恢复下一次的立即执行
    running = false;
    };
    return debounceEvent;
    };

七、获取防抖函数的返回值

  1. 先来手动调用一下点击处理函数

    <!-- test.html -->
    <!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 class="debounce">触发防抖事件</div>
    <div class="cancle">取消</div>
    <script src="./debounce.js"></script>
    <script>
    const clickEvent = function (e) {
    console.log("点击事件触发", e, this)
    }
    const debounceClickEvent = debounce(clickEvent, 2000, true)
    const cancle = () => { debounceClickEvent.cancle() }
    document.querySelector(".debounce").addEventListener("click", debounceClickEvent)
    document.querySelector(".cancle").addEventListener("click", cancle)
    // 手动调用点击处理函数
    debounceClickEvent()
    debounceClickEvent()
    debounceClickEvent()
    debounceClickEvent()
    </script>
    </body>
    </html>

    我们在上面手动调用了4次点击事件的处理函数,查看控制台也发现了打印了两次,一次是立即执行,一次是防抖处理的最后一次执行

    image-20240317134911297

  2. 给点击处理函数设置返回值

    下面,我们给clickEvent方法设置一个返回值。

    <!-- test.html -->
    <!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 class="debounce">触发防抖事件</div>
    <div class="cancle">取消</div>
    <script src="./debounce.js"></script>
    <script>
    const clickEvent = function (e) {
    console.log("点击事件触发", e, this)
    const result = "请求结果数据"
    return result
    }
    const debounceClickEvent = debounce(clickEvent, 2000, true)
    const cancle = () => { debounceClickEvent.cancle() }
    document.querySelector(".debounce").addEventListener("click", debounceClickEvent)
    document.querySelector(".cancle").addEventListener("click", cancle)
    // 手动调用点击处理函数
    console.log(debounceClickEvent())
    console.log(debounceClickEvent())
    console.log(debounceClickEvent())
    console.log(debounceClickEvent())
    </script>
    </body>
    </html>

    然后需要在debounceEvent方法中将结果返回

    // debounce.js
    const debounce = (fun, time, immediately = false) => {
    let timer;
    // 是否已经立即执行
    let running = false;
    // 恢复立即执行的定时器
    let timerRunning;
    const debounceEvent = function debounceEvent(...args) {
    // 返回结果
    let result;
    timer && clearTimeout(timer);
    // 开启了立即执行并且还没有立即执行,则说明这是第一次,直接立即执行
    if (immediately && !running) {
    // 第一次已经立即执行了,后面再次触发就不能再立即执行了
    running = true;
    // 获取方法执行的返回结果
    result = fun.apply(this, args);
    // 第一次立即执行已经完成了,我们在一段时间后恢复立即执行
    timerRunning = setTimeout(() => {
    running = false;
    }, time);
    } else {
    // 如果频繁触发了事件,则不恢复立即执行,而是等最后一次处理方法完成再恢复立即执行
    timerRunning && clearTimeout(timerRunning);
    timer = setTimeout(() => {
    // 获取方法执行的返回结果
    result = fun.apply(this, args);
    timer = null;
    // 最后一次防抖方法完成后,下一次还可以立即执行
    running = false;
    }, time);
    }
    // 返回结果
    return result;
    };
    debounceEvent.cancle = () => {
    console.log("清除定时器");
    timer && clearTimeout(timer);
    timer = null;
    // 取消最后一次防抖方法后,恢复下一次的立即执行
    running = false;
    };
    return debounceEvent;
    };

    查看控制输出

    image-20240317140343576

    第一次是立即执行的,所以可以拿到返回值。但最后一次是异步执行的,所以拿不到。可以使用Promise来处理

    // debounce.js
    const debounce = (fun, time, immediately = false) => {
    let timer;
    // 是否已经立即执行
    let running = false;
    // 恢复立即执行的定时器
    let timerRunning;
    const debounceEvent = function debounceEvent(...args) {
    return new Promise((resolve, reject) => {
    // 返回结果
    let result;
    timer && clearTimeout(timer);
    // 开启了立即执行并且还没有立即执行,则说明这是第一次,直接立即执行
    if (immediately && !running) {
    // 第一次已经立即执行了,后面再次触发就不能再立即执行了
    running = true;
    result = fun.apply(this, args);
    resolve(result);
    // 第一次立即执行已经完成了,我们在一段时间后恢复立即执行
    timerRunning = setTimeout(() => {
    running = false;
    }, time);
    } else {
    // 如果频繁触发了事件,则不恢复立即执行,而是等最后一次处理方法完成再恢复立即执行
    timerRunning && clearTimeout(timerRunning);
    timer = setTimeout(() => {
    result = fun.apply(this, args);
    resolve(result);
    timer = null;
    // 最后一次防抖方法完成后,下一次还可以立即执行
    running = false;
    }, time);
    }
    });
    };
    debounceEvent.cancle = () => {
    console.log("清除定时器");
    timer && clearTimeout(timer);
    timer = null;
    // 取消最后一次防抖方法后,恢复下一次的立即执行
    running = false;
    };
    return debounceEvent;
    };
    <!-- test.html -->
    <!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 class="debounce">触发防抖事件</div>
    <div class="cancle">取消</div>
    <script src="./debounce.js"></script>
    <script>
    const clickEvent = function (e) {
    console.log("点击事件触发", e, this)
    const result = "请求结果数据"
    return result
    }
    const debounceClickEvent = debounce(clickEvent, 2000, true)
    const cancle = () => { debounceClickEvent.cancle() }
    document.querySelector(".debounce").addEventListener("click", debounceClickEvent)
    document.querySelector(".cancle").addEventListener("click", cancle)
    // 手动调用点击处理函数
    // console.log(debounceClickEvent())
    // console.log(debounceClickEvent())
    // console.log(debounceClickEvent())
    // console.log(debounceClickEvent())
    debounceClickEvent().then(res => console.log(res))
    debounceClickEvent().then(res => console.log(res))
    debounceClickEvent().then(res => console.log(res))
    debounceClickEvent().then(res => console.log(res))
    </script>
    </body>
    </html>

    查看控制台输出,这下就没问题了

    image-20240317140944835

八、完整代码

<!-- test.html -->
<!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 class="debounce">触发防抖事件</div>
<div class="cancle">取消</div>
<script src="./debounce.js"></script>
<script>
const clickEvent = function (e) {
console.log("点击事件触发", e, this)
const result = "请求结果数据"
return result
}
const debounceClickEvent = debounce(clickEvent, 2000, true)
const cancle = () => { debounceClickEvent.cancle() }
document.querySelector(".debounce").addEventListener("click", debounceClickEvent)
document.querySelector(".cancle").addEventListener("click", cancle)
// 手动调用点击处理函数
// console.log(debounceClickEvent())
// console.log(debounceClickEvent())
// console.log(debounceClickEvent())
// console.log(debounceClickEvent())
debounceClickEvent().then(res => console.log(res))
debounceClickEvent().then(res => console.log(res))
debounceClickEvent().then(res => console.log(res))
debounceClickEvent().then(res => console.log(res))
</script>
</body>
</html>
// debounce.js
const debounce = (fun, time, immediately = false) => {
let timer;
// 是否已经立即执行
let running = false;
// 恢复立即执行的定时器
let timerRunning;
const debounceEvent = function debounceEvent(...args) {
return new Promise((resolve, reject) => {
// 返回结果
let result;
timer && clearTimeout(timer);
// 开启了立即执行并且还没有立即执行,则说明这是第一次,直接立即执行
if (immediately && !running) {
// 第一次已经立即执行了,后面再次触发就不能再立即执行了
running = true;
result = fun.apply(this, args);
resolve(result);
// 第一次立即执行已经完成了,我们在一段时间后恢复立即执行
timerRunning = setTimeout(() => {
running = false;
}, time);
} else {
// 如果频繁触发了事件,则不恢复立即执行,而是等最后一次处理方法完成再恢复立即执行
timerRunning && clearTimeout(timerRunning);
timer = setTimeout(() => {
result = fun.apply(this, args);
resolve(result);
timer = null;
// 最后一次防抖方法完成后,下一次还可以立即执行
running = false;
}, time);
}
});
};
debounceEvent.cancle = () => {
console.log("清除定时器");
timer && clearTimeout(timer);
timer = null;
// 取消最后一次防抖方法后,恢复下一次的立即执行
running = false;
};
return debounceEvent;
};
posted @   平平丶淡淡  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起