函数防抖知识要点

2023-11-19

函数防抖 debounce

JavaScript 中的函数大多数情况下都是由用户主动调用触发的,比如说点击、拖拽、改变浏览器尺寸、提交表单等。除非是函数本身的实现不合理,否则一般不会遇到跟性能相关的问题。

但是在一些少数情况下,函数的触发不是由用户直接控制的。在这些场景下,函数有可能被非常频繁地调用,而造成性能问题:

  • mousemove 事件。如果要实现一个拖拽功能,需要一路监听 mousemove 事件,在回调中获取元素当前位置来进行样式改变。如果不加以控制,每移动一定像素而触发的回调数量非常惊人,回调中又伴随着 DOM 操作,继而引发浏览器的重排与重绘,性能差的浏览器可能就会直接假死。

  • window.onresize 事件。为 window 对象绑定了 resize 事件,当浏览器窗口大小被拖动而改变尺寸的时候,这个事件就会一直被触发。如果在 window.onresize 事件函数里又有一些跟 DOM 节点相关的操作,浏览器可能就会吃不消而造成卡顿现象。

  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)不能因为连续点击或快速按键连续发射多颗子弹。

  • 搜索联想(keyup事件)

  • 监听滚动(scroll事件)

解决这些情况的方案就可以使用函数防抖(debounce),其核心就是限制某一个方法的频繁触发。防止函数在极短的时间内反复调用,造成资源的浪费。

场景设想

场景1

考虑一下电梯关门的场景,现代的大部分电梯都可以通过红外,感知到是否有人进入。为了避免夹到人,同时为了等待后面的人,电梯关门的时间往往有一种规则:**始终保证电梯门在最后一个人进入后 3 秒以后关闭。**如果有人进入后,还没有等到 3 秒,又有人进来了,电梯门会以最后一次进入的时间为计时起点,重新等待3秒。

场景2

再考虑一个页面上的场景。页面上的某些事件触发频率非常高,比如滚动条滚动、窗口尺寸变化、鼠标移动等,如果我们需要注册这类事件,不得不考虑效率问题,又特别是事件处理中涉及到了大量的操作,比如:

window.onresize = function(){
    // 大量的 DOM 操作
}

当窗口尺寸发生哪怕一丢丢变化时,都会造成多次对处理函数的调用,这对网页性能的影响是极其巨大的。于是,我们可以考虑,每次窗口尺寸变化、滚动条滚动、鼠标移动时,不要立即执行相关操作,而是等一段时间,以窗口尺寸停止变化、滚动条不再滚动、鼠标不再移动为计时起点,一段时间后再去执行操作,就像电梯关门那样。

场景3

再考虑一个搜索的场景,当用户在一个文本框中输入文字(keydown 事件)时,需要将文字发送到服务器,并从服务器得到搜索结果。这样的话,用户直接输入搜索文字就可以了,不用再去点搜索按钮,可以提升用户体验:

img

可是如何来实现上面的场景呢?如果文本框的文字每次被改变(keydown 或者 keyup),都要把数据发送到服务器,得到搜索结果,想想看,当搜索 “google” 这样的单词,至少需要按 6 次按键,就这一个词就需要向服务器请求 6 次,并让服务器去搜索 6 次,但其实只需要最后一次的结果就可以了。如果考虑用户按错的情况,发送请求的次数更加恐怖,浪费了很多资源。

所以真正的搜索行为并不是每次输入都会触发的,只有当用户停止按键一段时间后才会触发。

推测代码

为了满足这种类型场景,可以设计需要满足以下功能:

  1. 调用该函数后,不立即做事,而是一段时间后去做事
  2. 如果在等待时间内调用了该函数,则重新开始计时

这样的功能,就叫做函数防抖,其实就是防止函数短时间内被调用多次。要完成该函数,需要给予两个条件:

  1. 告诉我一段时间后要做什么事(这里应该是一个 callback,即函数作为参数)
  2. 告诉我要等待多长时间

那思考一下,什么东西可以 “在某个时间后触发一次” ?延时型计时器 setTimeout!没错,所以……代码可能?可以写成:

// 省略了标签 <input type="text" id='txt'> 别问为啥,就是想。
txt.onkeyup = function () {
  setTimeout(() => {
    console.log(txt.value);
  }, 3000); // 3s 后执行
}

img

通过结果会发现,当输入一个字符时,确实可以实现 3s 后 log 出值,但当连续输入,就会“囤积”多个计时器在最后统一执行,就像第二次我输入了 5 个字符,3s 后连续 log 了5 次。

那为了停止计时器,代码就又可以改成:

let timer = null; // 设置全局变量,记录 setTimeout 得到的id
txt.onkeyup = function () {
  if (timer) { // 如果 timer 有值(是一个 setTimeout 的函数)
    clearTimeout(timer);
  }
  timer = setTimeout(() => {
    console.log(txt.value);
  }, 3000);
}

img

看起来可以满足需求了,哪怕是连续输入,也不会一次性囤几个计时器在最后时刻统一触发。

但当前对于全局 timer 变量,它意味着页面中所有需要防抖的地方,都共用了这个 timer,假设需要同时对 resize 事件和文本框 keyup 事件进行防抖,看来就不行了。

计时器中做的事儿是可变的(计时器第一个参数 function 的函数体内容可变),延时的时间也是可变的,所以可以封装成函数进行传参:

/*
	代码分解:
	1. timer 的声明、timer 的判断、timer 的赋值都是固定的,所以可以丢进 function debounce(){}里
	2. txt.onkeyup 的事件处理函数,做的就是调用 debounce() 这个事
	3. 既然 debounce() 是个函数,必定可以传参,所以将会发生变化的在计时器里做的事作为实参传给 debounce(),再将间隔时间作为实参传递
*/
txt.onkeyup = function () {
  debounce(function () {
    console.log(txt.value);
  }, 3000);
}

function debounce(fn, ms) { // fn 是函数调用时的第一个函数参数
  let timer = null;
  if (timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(() => {
    fn();
  }, ms);
}

但这样的运行结果同上面一样,会“囤积”多个计时器一起触发,每次调用防抖函数 debounce() 都会得到一个崭新的计时器(let timer = null)

所以代码可能可以改成:

function debounce(fn, ms) {
  let timer = null;
  // 把防抖的工作 return 出去,这样一来 timer 每次都是新的了
  return function () {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn();
    }, ms);
  }
}

debounce 函数仅仅负责生成一个计时器的 timerId,保证每次调用 debounce 都会有一个新的 timerId 产生即可,将防抖的工作,交给了返回的函数去完成。今后如果需要对某一个操作进行防抖,只需要调用 debounce() 来得到一个操作函数,再去调用操作函数即可。

// 防抖函数
function debounce(fn, ms) {
  let timer = null;
  return function () {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn();
    }, ms);
  }
}
// 真正操作防抖 textChange 得到的是一个函数
let textChange = debounce(function () { // function 参数作为 debounce 的第一个实参
  console.log(txt.value);
}, 3000);

txt.onkeyup = function () {
  textChange();
}

假设当前同时需要对输入框的 keyup 和 窗口的 resize 防抖:

let textChange = debounce(() => {
  console.log(txt.value);
}, 3000);
txt.onkeyup = () => {
  txtChange();
}

let winChange = debounce(() => {
  console.log("窗口尺寸改变了");
}, 500);
window.onresize = () => {
  winChange();
}

img

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

函数防抖知识要点 的相关文章

  • 如何向 CSS 形状添加偏移轮廓?

    我在创建带有斜角边缘的块时遇到了一些问题 此外我需要一个斜角的边框并稍微偏离主块 问题是这个块可以根据屏幕响应 不知道具体的方法 希望大家帮忙 这就是我现在所做的 box display flex padding 20px height 2
  • jQuery 或 Javascript - 如何禁用窗口滚动而不溢出:隐藏;

    您好 是否可以在不使用的情况下禁用窗口滚动overflow hidden 当我悬停一个元素时 我试过 chat content on mouseenter function document scroll function e if e h
  • 从 html 属性中删除单引号和双引号,并且除 href 和 src 之外的所有属性上都没有空格

    我正在尝试从 html 属性中删除单引号和双引号 这些属性是没有空格的单个单词 我写了这个有效的正则表达式 type title data toggle colspan scope role media name rel id class
  • HTML5

    我想在随机位置开始和停止 HTML5 播放 并具有淡入和淡出周期 以平滑聆听体验 为此存在什么样的机制 使用 setTimeout 手动增加音量 jQuery 的方式 audio animate volume newVolume 1000
  • .Net 中是否有与 HTML 等效的 XmlReader?

    我用过Html敏捷包 http html agility pack net z codeplex过去在 Net 中解析 HTML 但我不喜欢它只使用 DOM 模型 在大型文档和 或具有大量嵌套的文档上 可能会遇到堆栈溢出或内存不足异常 另外
  • 滚动时将菜单栏固定在顶部[关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我见过一些网站 当用户向下滚动页面时 会在右侧或左侧弹出一个框 另外 注意到这个模板 http www mvpthemes com m
  • 我应该使用哪种文档类型?

    如果我想使用可定制的 div 我应该使用哪种文档类型 具有div动画 图像移动 设置div不透明度等 我尝试通过 javascript 创建一个 div 设置其背景颜色 位置 宽度和高度 并向其添加 onmouseover 事件 一切正常
  • Django:使用条件 {% extends %} 使 {% block "div" %} 成为条件

    我想分享一个 AJAX 和常规 HTTP 调用之间的模板 唯一的区别是一个模板需要扩展 base html html 而另一个则不需要 我可以用 extends request is ajax yesno app base ajax htm
  • 使用 jQuery 更改父元素样式

    我有下一个 html 设置 div class one div class two a href class three Click a div div 我想更改具有类的元素的背景颜色 one当我点击元素时 three使用 jQuery 这
  • 如何在 Bootstrap 3 的导航栏中添加带有图标的搜索框?

    我正在使用新的 Twitter Bootstrap 3 并尝试放置一个像这样的搜索框 如下 在顶部导航栏中 在 Bootstrap 2 中 可以这样完成
  • Angular UI select:从远程服务获取数据

    我正在使用角度用户界面选择 https github com angular ui ui select https github com angular ui ui select 我查看了演示的可用位置这个笨蛋 http plnkr co
  • 如何让Gmail像加载进度条一样

    我想在页面的中心和顶部创建一个像 Gmail 一样的加载进度条 并适用于所有浏览器 这是基本代码
  • 在随机位置启动 HTML5

    我有一个大约 2 小时长的音轨 我想在我的网站上使用它 我希望它在页面加载时在随机位置开始播放曲目 使用 HTML5 可以吗 我知道您可以使用 element currentTime 函数来获取当前位置 但是如何在完全下载之前获取曲目的总时
  • 我怎样才能让这个脚本在 WordPress 上运行?

    我有这个脚本 document ready function text1 click function this hide 代码html div class div1 p class text1 text to appear when th
  • 为什么此 TTF 字体在我的浏览器中无法使用?

    我下载了一种名为 Clunk 的 TTF 字体 并尝试将其应用于某些文本 这是我正在使用的代码 h1 Test h1 这似乎不起作用 Chrome 给了我两个错误 Failed to decode downloaded font path
  • 更改API数据输出的布局

    我是 API 集成和 PHP 的新手 我最近将 VIN 解码器集成到我的应用程序中 在输入框中输入车辆的 VIN 选择提交 然后就会显示 API 数据库中有关该车辆的所有信息 数据存储为关联数组 其中包含类别及其相应元素 例如 对于 VIN
  • HTML 代码中的 PHP [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我用 HTML 代码编写了 PHP div div 但这出现在输出页面中 else print 我怎样才能让PHP执行 你的文件有一个 p
  • 内嵌显示定义术语和描述

    我正在为页面上的某些元素使用定义列表 并需要它们内联显示 例如 它们normally看起来像 我需要它们看起来像 注意多个 DD 我可以让它们在 moz 中使用 float 来正常工作 但无论我尝试什么 它们都无法在 IE 中工作 我通常会
  • 如何在画布上所有其他内容后面绘制图像? [复制]

    这个问题在这里已经有答案了 我有一块画布 我想用drawImage在画布上当前内容后面绘制图像 由于画布上已经有内容 我正在使用字面上的画布来创建包含图像的画布 因此我无法真正先绘制图像 所以我无法使用drawImage在我呈现其余内容之前
  • 如何垂直对齐div内的图像

    如何在包含的内容中对齐图像div Example 在我的示例中 我需要将 img in the div with class frame div class frame style height 25px img src http jsfi

随机推荐

  • Windows安装frida

    一 正常步骤 cmd中 pip3 install frida i https pypi mirrors ustc edu cn simple 上面失败用这个 pip install frida i http mirrors aliyun c
  • linux 查看及修改字符集

    一 查看当前linux系统的字符集方法 1 1 locale 1 2 echo LANG 1 3 env grep LANG 二 查看当前系统支持的字符集 root localhost locale a 三 修改系统字符集 3 1 临时生效
  • vue中使用bus总线在非父子组件之间传值

    使用bus总 线可以在 兄弟 父子 祖先和后代 组件之间传值 原理 在Vue原型中 创建一个bus属性 让每一个组件 实例 都具有这个属性 这里自行引入 vue
  • Idea 发布最适合程序员的字体!

    作为 编译期界的大佬 JetBrains公司一直致力于提供更好的编码环境 前两天 JetBrain推出了一个新的字体 JetBrain Mono 号称是最适合程序员的编码的字体 我赶紧尝了尝鲜 体验了一天之后发现确实好看 因此推荐给大家 首
  • ABB MPRC086444-005数字输入模块

    ABB MPRC086444 005 是一款数字输入模块 通常用于工业自动化和控制系统中 用于接收和处理数字信号 以下是这种类型的数字输入模块通常可能具备的一般功能和特点 数字输入接口 MPRC086444 005 模块通常配备多个数字输入
  • AttGAN从paper到code理解

    AttGAN Facial Attribute Editing by Only Changing What You Want 2017 CVPR 文章简介 本文研究面部属性编辑任务 其目的是通过操作单个或多个感兴趣的属性 如头发颜色 表情
  • RabbitMQ的安装和启动——windows版

    本章介绍如何在win10下安装 RabbitMQ 并启动 安装 Erlang 安装 RabbitMQ 启动 RabbitMQ 百度网盘下载 Erlang 和 RabbitMQ Erlang 网盘链接 RabbitMQ 网盘链接 结语 下载R
  • 搜索研发工程师需要掌握的一些技能

    文章目录 基础 语言 数据结构与算法 工程方面 搜索相关 搜索主要模块 电商搜索流程 分词相关 搜索召回 相似度算法 相关词推荐 排序相关 国美搜索 搜索算法工程师需要掌握的技能 基础 语言 大部分公司用的是Solr ElasticSear
  • 算法相关-经典排序算法(python实现)

    概述 插入排序 将未排序的元素同已排序的元素从后往前比较 带排序元素 a 被比较元素 b 如果a
  • 在Linux是使用libxml2---从安装到使用

    一 下载和安装LIBXML2 方法一 Libxml2是个C语言的XML程式库 能简单方便的提供对XML文件的各种操作 并且支持XPATH查询 及部分的支持XSLT转换等功能 Libxml2的下载地址是 http xmlsoft org 完全
  • 支撑区块链大规模商用,FISCO BCOS v3.0的那些“黑科技”

    注 文章转载自CSDN公众号 在2021年度金链盟生态大会上 全新的FISCO BCOS v3 0正式发布 该版本从架构 算法以及安全可控和隐私计算协同等方向进行了全面升级 满足数字经济时代对区块链系统可承载更大规模 更多场景 更广泛参与的
  • spring-xxx-xxx-0.0.1-SNAPSHOT.jar中没有主清单属性完美解决

    这种情况就是因为没有在SpringBoot中pom文件安装maven plugin 导致出现没有主清单属性问题 1 引入插件
  • JPush极光推送Unity插件iOS设备无法获取DeviceToken

    前言 最近在使用JPush进行极光推送 Unity插件GitHub地址https github com jpush jpush unity3d plugin 问题描述 但是发现了一个问题 按照官方文档操作 最终仍然无法获取DeviceTok
  • Error: unable to connect to node rabbit@localhost: nodedown

    刚安装上rabbimq 当我使用rabbitmqctl start app 启动rabbitmq的时候 出现了如下问题Error unable to connect to node rabbit localhost nodedown 然后我
  • vue-quill-editor复制粘贴问题

    需求是这样的 富文本可以具备粘贴文本的功能 但是不能粘贴图片到编辑框中 于是百度一下很快就有了解决方案 在data的文本编辑框配置中添加一个clipboard粘贴板 对其进行配置 方法中可以自觉将粘贴的图片转换为空的字符串 所以巧妙实现了此
  • 解决ubuntu打不开软件更新器和软件中心的问题

    打不开可能是软件源的问题 试试 sudo gedit etc apt sources list 然后把第三方软件源全部删除掉 重启软件更新器 如果能够启动 但有提示请检查网络连接的信息 那么点设置 在其它软件选项卡看情况取消勾选一些软件源
  • 解决报错:无法使用 JSX,除非提供了 “--jsx“ 标志。ts(17004)

    在 vue cli 5 0 6下创建项目 打开代码突然出现 无法使用 JSX 除非提供了 jsx 标志 之前都没问题 今天打开一看就报错了 网上说是 typescript的本地版本和你当前项目的版本不一致 请将本地的ts版本更新至项目需要的
  • 【BrokenPipeError: [Errno 32] Broken pipe】的解决方案

    BrokenPipeError Errno 32 Broken pipe 的解决方案 项目场景 问题描述 原因分析 解决方案 End 项目场景 调试 GitHub项目bddoia project Explainable Object ind
  • Docker一运行容器就退出:已解决

    Docker一运行容器就退出 已解决 文章目录 Docker一运行容器就退出 已解决 问题引入 解决方法 问题引入 想着挂载一个数据卷人挪活设置个端口号在外部访问一下 结果刚刚运行就停止了 如下图所示 就算是给它加上做一个死循环 持续输出
  • 函数防抖知识要点

    函数防抖 debounce JavaScript 中的函数大多数情况下都是由用户主动调用触发的 比如说点击 拖拽 改变浏览器尺寸 提交表单等 除非是函数本身的实现不合理 否则一般不会遇到跟性能相关的问题 但是在一些少数情况下 函数的触发不是