前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >几分钟学会手搓防抖

几分钟学会手搓防抖

作者头像
用户6256742
发布2024-08-01 09:18:54
1120
发布2024-08-01 09:18:54
举报
文章被收录于专栏:网络日志

防抖动简述

防抖动(debounce)是一种用于优化前端性能的技术,主要应用于需要限制函数执行频率的场景。其核心思想是延迟函数执行,直到一段时间内没有新的触发事件发生,然后再执行该函数。

具体实现方式是通过设置一个定时器,在函数被触发时启动计时器,如果在指定的时间间隔内函数再次被触发,则重新计时。只有当定时器结束并且在间隔期间没有新的触发事件发生时,才执行函数。

举例来说,假设我们有一个输入框,用户每输入一个字符就会触发一个输入事件,我们希望在用户停止输入后的500毫秒再执行输入验证函数。这时候就可以利用防抖动的思想,在输入事件触发时启动一个500毫秒的定时器,在每次输入事件触发时重新计时。只有当用户停止输入并且500毫秒内没有新的输入事件发生时,才执行输入验证函数,从而减少了函数执行的频率。

防抖动常用于输入框输入验证、滚动事件处理等需要限制函数执行频率的场景,可以有效减少不必要的计算和提升页面性能。

防抖的核心原理

防抖的核心原理是通过设置定时器来延迟函数的执行,在指定的时间间隔内,如果函数再次被触发,则重新计时。只有当定时器结束并且在间隔期间没有新的触发事件发生时,才执行函数。

具体步骤如下:

  1. 当事件触发时,清除之前设置的定时器(如果有)。
  2. 启动一个新的定时器,在指定的时间间隔内等待。
  3. 如果在等待期间再次触发了事件,重复步骤1和步骤2。
  4. 如果定时器到期并且在等待期间没有新的触发事件发生,执行函数。

这样就保证了在频繁触发事件时,只有最后一次触发事件被处理,而其他触发事件被忽略,从而达到减少函数执行次数和提升性能的效果。

手搓防抖过程

手搓一个防抖实现在频繁点击提交按钮时,只有最后一次点击提交按钮被处理,而之前点击的提交按钮被忽略。

初步尝试

代码语言:javascript
复制
var btn = document.getElementById('btn');
btn.addEventListener('click', () => {
    setTimeout(() => {
        console.log('提交');
    }, 1000)
})
  1. 我们获取id为"btn"的按钮元素,并赋值给变量btn。
  2. addEventListener方法为给按钮添加一个点击事件监听器,当按钮被点击就执行回调函数。
  3. 回调函数中有一个由setTimeout函数设置的定时器,延迟一秒后执行其中的回调函数。

但是这样并没有实现防抖。当用户点击几次就会执行几次回调函数,也就会执行相应次数的定时器的回调函数。

连续点击4下提交按钮:

几分钟学会手搓防抖
几分钟学会手搓防抖

输出了4次提交。

我们在这个基础上进行改进。

改进代码

代码语言:javascript
复制
function handle() {
    //ajax请求
    console.log('提交');
}
btn.addEventListener('click', debounce(handle))
function debounce(fn) {
    let timer = null;
    return function () {
        clearTimeout(timer)
        timer = setTimeout(fn, 1000)
    }
}

我们定义一个debounce函数实现防抖操作。定义了一个handle函数用于处理点击事件。

通过addEventListener方法给按钮添加了一个点击事件监听器,当用户点击按钮时触发防抖函数debounce返回的函数。

在debounce函数中,我们创建了一个timer变量并且赋值为null,然后返回一个函数。在返回的函数中实现了清除上一个计时器,然后重新设置一个计时器的操作。

你会发现返回的函数中使用了闭包来保存一个定时器timer的引用。

连续点击4下提交按钮:

几分钟学会手搓防抖
几分钟学会手搓防抖

实现了防抖。

让我们一起化身为JavaScript引擎执行一遍这段代码,深入了解防抖的实现。

图解

  1. 对全局代码进行预编译,全局执行上下文入栈。
几分钟学会手搓防抖
几分钟学会手搓防抖
  1. 执行addEventListener函数,addEventListener函数执行上下文入栈。
几分钟学会手搓防抖
几分钟学会手搓防抖
  1. addEventListener函数调用debounce函数,debounce函数执行上下文入栈。
几分钟学会手搓防抖
几分钟学会手搓防抖
  1. dobounce函数执行完成返回一个匿名函数,addEventListener函数将返回的匿名函数绑定到按钮(btn)的点击事件上。然后dobounce函数执行上下文和addEventListener函数执行上下文分别出栈。 但是dobounce函数返回的匿名函数需要引用dobounce函数说明的timer变量。 所以dobounce函数会留下一个小背包(闭包)存放timer变量,让匿名函数可以访问timer变量。
几分钟学会手搓防抖
几分钟学会手搓防抖
  1. 等待点击事件触发匿名函数。当点击事件发生匿名函数执行上下文入栈。
几分钟学会手搓防抖
几分钟学会手搓防抖
  1. 如果频繁点击提交按钮,闭包中的timer计时器就会被重复被清除后重置,以至于无法调用handle函数。
  2. 在频繁点击提交按钮时,只有最后一次点击提交按钮被处理。最后一次点击提交按钮,handle函数被调用,handle函数执行上下文入栈。
几分钟学会手搓防抖
几分钟学会手搓防抖

发现漏洞

this指向被篡改

输出handle的this指向。

代码语言:javascript
复制
function handle() {
    //ajax请求
    console.log('提交',this);
}
btn.addEventListener('click', debounce(handle))
function debounce(fn) {
    let timer = null;
    return function () {
        clearTimeout(timer)
        timer = setTimeout(fn, 1000)
    }
}

连续点击提交按钮4次:

几分钟学会手搓防抖
几分钟学会手搓防抖

输出结果表明handle的this指向是window。

让我们分析handle的this指向原本应该指向什么:

addEventListener决定handle的执行,所以addEventListener干扰了this,让ajax请求this不指向全局,而是指向btn。这是合理的,也是应该的。

但是是计时器函数执行的fn函数,计时器函数的this是指向window,ajax请求的this也指向window。

我们实现防抖,但是不能改变原有的this指向。我们需要将this指向纠正。

纠正this指向法一:箭头函数
代码语言:javascript
复制
var btn = document.getElementById('btn');

function handle() {
    console.log('提交', this);
}

btn.addEventListener('click', debounce(handle))

function debounce(fn) {
    let timer = null;
    return function () {
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.call(this)
        }, 1000)
    }
}

我们将计时器中的回调函数改为箭头函数,并且在箭头函数内对handle函数的this指向进行显式绑定。

因为箭头函数没有this,所以 fn.call(this)中的this是匿名函数的this。也就是说handle函数的this指向和匿名函数的this指向是一样的。

又因为匿名函数是addEventListener方法的回调函数,addEventListener会使其回调函数的this指向btn,所以匿名函数的this指向btn。handle也跟着匿名函数也指向btn。

我们连续点击4下提交按钮:

几分钟学会手搓防抖
几分钟学会手搓防抖

最终成功纠正handle的this指向,指向btn。

纠正this指向法二:保存 this 引用
代码语言:javascript
复制
var btn = document.getElementById('btn');

function handle() {
    console.log('提交', this);
}

btn.addEventListener('click', debounce(handle))

function debounce(fn) {
    let timer = null;
    return function () {
        const that = this
        clearTimeout(timer)
        timer = setTimeout(function () {
            fn.call(that)
        }, 1000)
    }
}

我们找到匿名函数是addEventListener的回调函数,其this指向的是btn。我们可以证明一下。

证明:

代码语言:javascript
复制
var btn = document.getElementById('btn');
function handle() {
    console.log('提交', this);
}
btn.addEventListener('click', handle)

结果是:

几分钟学会手搓防抖
几分钟学会手搓防抖

我们用that变量保存匿名函数的this指向,并且将handle的this指向显式绑定为匿名函数的this指向,也就是指向btn。

我们连续点击4下提交按钮:

几分钟学会手搓防抖
几分钟学会手搓防抖

最终成功纠正handle的this指向,指向btn。

事件对象(e)被篡改

输出addEventListener回调函数的事件对象(e):

代码语言:javascript
复制
var btn = document.getElementById('btn');

function handle(e) {
    console.log('现在的事件对象', e);
}

btn.addEventListener('click', debounce(handle))

function debounce(fn) {
    let timer = null;
    return function (e) {
        console.log('原来的事件对象', e);
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.call(this)
        }, 1000)
    }
}

点击提交按钮1次查看原来的事件对象(e)和最终输出的事件对象(e):

几分钟学会手搓防抖
几分钟学会手搓防抖

最终的事件对象被篡改了。我们需要纠正回来。

纠正事件对象
代码语言:javascript
复制
var btn = document.getElementById('btn');

function handle(e) {
    console.log('现在的事件对象', e);
}

btn.addEventListener('click', debounce(handle))

function debounce(fn) {
    let timer = null;
    return function (e) {
        console.log('原来的事件对象', e);
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn.call(this, e)
        }, 1000)
    }
}

在 debounce函数内部的 setTimeout回调中,通过 fn.call(this, e) ,将原始事件对象e传递给了 handle 函数,这样就确保了handle函数接收到的是正确的事件对象。

点击一次提交按钮:

几分钟学会手搓防抖
几分钟学会手搓防抖

最终纠正了事件对象。

最终版手搓防抖代码

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>

    <body>
        <button id="btn">提交</button>
        <script>
            var btn = document.getElementById('btn');

            function handle(e) {
                console.log('提交');
            }

            btn.addEventListener('click', debounce(handle))

            function debounce(fn) {
                let timer = null;
                return function (e) {         
                    clearTimeout(timer)
                    timer = setTimeout(() => {
                        fn.call(this, e)
                    }, 1000)
                }
            }
        </script>
    </body>
</html>

小结

在写防抖操作时,不要忘记了还原原函数的this指向和还原原函数的事件对象e。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-06-12 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 防抖动简述
  • 防抖的核心原理
  • 手搓防抖过程
    • 初步尝试
      • 改进代码
        • 发现漏洞
          • this指向被篡改
          • 事件对象(e)被篡改
        • 最终版手搓防抖代码
        • 小结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档