本文讨论前端开发中 函数防抖 和 函数节流,它们的应用、区别以及简单实现。

在前端开发中我们可能经常需要给(页面)标签绑定一些持续触发的事件,如 resizescrollinputmousemovekeyupkeydown 等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

譬如,如果用户有频繁的resizescroll 行为,那么会导致页面不断的被重新渲染,如果在绑定的回调函数中存在大量的 DOM 操作,那么还会出现页面的卡顿,针对这种情况,常用的解决方式就是利用节流( throttle )防抖( debounce )来优化高频事件,降低代码的执行频率。

若想对比默认情况、函数节流和函数防抖的情况,可以参考演示效果-点击我来直观感受它们的区别。

函数节流
    <!-- 原先: 1秒 执行 100次 -->
<!-- 调节: 1秒 执行 1次-->
<!-- 10秒钟执行1000次任务调整后10秒钟最多执行10次 -->
<!-- 换个例子 -->
<!-- 原先: 1秒中从池塘中流水100L -->
<!-- 调节: 1秒钟从池塘中流水1L -->
<!-- 10秒钟流出1000L水调整后10秒钟最多流出10L 水,这就是节流的操作。 -->

函数节流 可以通过时间戳来实现。

下面,我们试着以代码的方式来探究函数节流的细节和具体实现。

我们在页面中提供一个按钮,给按钮绑定点击事件,那么正常情况是每当按钮点击一次的时候,对应的事件处理函数就会被触发执行一次。

    /* 页面标签: <button>按钮</button>  */
let task = (e) => console.log("click button", e); let oBtn = document.querySelector("button");
oBtn.addEventListener("click", task);

如果用户在短时间内快速连续多次的点击按钮,那么事件处理函数也会随之触发很多次。函数节流规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。换言之,函数节流控制在固定的时间单位内,事件任务只会执行(生效)一次。

   /* 事件处理函数 */
let task = (e) => console.log("click button", e); /* 函数节流 */
function throttle(func, wait) {
let previous = 0; return function() {
context = this;
args = arguments; let now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
} /* 绑定事件 */
let oBtn = document.querySelector("button");
oBtn.addEventListener("click", throttle(task, 1000));

上面给出了函数节流的简单实现方式,代码中封装了throttle 函数,该函数接收任何(事件)函数和间隔时间两个参数,并返回一个新在函数中。throttle 函数的核心是,在返回的函数中通过获取当前时间戳并和间隔时间进行比较的方式来控制是否应该执行任务函数。

当事件处理函数第一次执行后,后续点击事件被触发的时候,如果now - previous > wait 成立(距离上次事件触发的时间已经超过了指定间隔时间),那么则执行任务函数,否则就忽略这次点击事件。注意func.apply(context, args)这行代码的作用是,把具体的标签绑定给事件处理函数中的this, 此外在事件处理函数中可能还会存在事件对象等参数的传递,需要考虑到这种情况。

关于函数节流的代码实现,我们还可以阅读和参考下知名框架[ underscore ](https://github.com/jashkenas/underscore/blob/master/underscore.js)的写法,该框架对函数节流提供了更精确的控制,譬如可以通过传递参数的方式来控制 第一次点击事件是否生效,以及最后一次的点击是否要触发等,下面给出其函数节流代码的核心实现。
    function throttle(func, wait, options) {
let timeout,args, context, previous = 0, let throttled = function() {
context = this;
args = arguments; let now = Date.now(); /* 该行代码设置第一次点击不生效 */
if (!previous && options.leading === false) previous = now; let remaning = wait - (now - previous); /* 如果:是第一次触发事件 */
/* 那么:执行事件处理函数,并更新previous值,如果有定时器,那么就进行清理操作 */
if (remaning <= 0) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
func.apply(context, args);
previous = now; } else if (!timeout && options.trailing !== false) {
/* 如果:不是第一次触发事件 && 定时器为空 && trailing == true */
/* 那么:总是执行最后一次的事件触发的处理函数 */
timeout = setTimeout(() =>{
previous = options.leading === false ? 0 : Date.now();
func.apply(context, args);
args = context = null
}, remaning);
}
} return throttled;
} /* 任何处理函数 */
function task(e) {
console.log('click', e);
} oBtn.addEventListener('click', throttle(task, 1000, {
leading: false,/* 设置为 false的时候,第一次点击不生效 */
trailing: true /* 设置最终一次点击总是触发 */
}));
函数防抖

函数防抖(debounce)就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会从头重新计算函数执行时间。

函数防抖 可以通过定时器来实现。

我们假设,当点击页面按钮的时候,在1秒的时间内事件处理函数只能执行一次,如果下次点击按钮的时候还没有超过1秒这个时间间隔,那么就重新开始计时。下面给出一份简单的代码实现供大家参考:

     /* <button></button> */
/* 任务执行函数 */
let task = (e) => console.log("task", e);
/* 防抖函数 */
function debounce(func, wait) {
let timer;
return function() {
clearTimeout(timer); /* 先清理以前的定时器(延迟函数) */
let context = this;
let args = arguments;
/* 开启定时器,指定时间后执行任务函数 task */
timer = setTimeout(() => {
func.apply(context, args);
timer = null;
}, wait);
}
}
/* 获取标签 */
let oBtn = document.querySelector("button");
/* 注册事件 */
oBtn.addEventListener("click", debounce(task, 1000));

稍微调整下上面的代码,假设我们想要通过一个参数来控制是否要在第一次触发事件的时候,执行任务函数,那么可以参考下面的写法:

   /* <button></button> */
/* 任务执行函数 */
let task = (e) => console.log("task", e); /*
防抖函数
func:具体的事件处理函数(任务函数)
wait:规定的时间(单位毫秒)
immediate:布尔型参数,开始的时候是否先执行一次
*/
function debounce(func, wait, immediate) {
let timer;
return function() { clearTimeout(timer); /* 清理以前的定时器(延迟函数) */
let context = this;
let args = arguments; /* 是否要在最开始的时候,先执行一次 */
if (immediate) {
let callNow = !timer;
if (callNow) func.apply(context, args);
} /* 开启定时器,指定时间后执行任务函数 task */
timer = setTimeout(() => {
func.apply(context, args);
timer = null;
}, wait);
}
} /* 获取标签 */
let oBtn = document.querySelector("button");
/* 注册事件 */
oBtn.addEventListener("click", debounce(task, 1000, true));

总结下,函数防抖函数节流都是防止某一事件的频繁触发,但原理却不一样:函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行

源码赏析

最后,简单贴出著名框架 lodash 中关于函数防抖和函数节流的部分演示代码,并附上 Github开源地址

/* debounce.js 文件实现 */
function debounce(func, wait, opts = {}) { let maxWait;
if ('maxWait' in opts) {
maxWait = opts.maxWait;
} let leading = true; // 第一次点击时触发
let trailing = true; // 最后一次也要触发 let lastCallTime; // 最后调用的时间 previous
let timeout;
let lastThis; // 返回函数的this
let lastArgs; // 返回函数的参数 // shouldInvoke 表示是否应该调用
let lastInvokeTime;
let shouldInvoke = function(now) {
let sinceLastTime = now - lastCallTime;
let sinceLastInvoke = now - lastInvokeTime;
// 第一次
return lastCallTime === undefined
|| sinceLastTime > wait
|| sinceLastInvoke >= maxWait;
} // leadingEdge 是否第一次执行
let invokeFunc = function(time) {
// 最终的调用函数的时间
lastInvokeTime = time;
func.apply(lastThis, lastArgs);
} // startTimer就是开启了一个定时器
let startTimer = function(timerExpired, wait) {
timeout = setTimeout(timerExpired, wait);
} let remainingWait = function(now) {
return wait - (now - lastCallTime);
} let trailingEdge = function(time) {
timeout = undefined;
if (trailing) {
invokeFunc(time);
}
} let timerExpired = function() {
let now = Date.now(); // 当前定时器到时间了 看看是否需要执行这个函数
if (shouldInvoke(now)) { // 如果需要调用,那么就触发结束的方法
return trailingEdge(now);
}
startTimer(timerExpired, remainingWait(now));
} let leadingEdge = function(time) {
lastInvokeTime = time; // 如果需要执行那么就调用函数
if (leading) {
invokeFunc(time)
}
} // 开启一个定时器 看下一次定时器到了 是否需要执行func
startTimer(timerExpired, wait); let debounced = function(...args) {
lastThis = this;
lastArgs = args;
let now = Date.now(); // 判断当前的debounce时是否需要执行
let isInvoking = shouldInvoke(now);
lastCallTime = now;
if (isInvoking) {
if (timeout === undefined) {
leadingEdge(now);
}
}
}
return debounced;
} /* throttle.js 文件实现 */
function throttle(func, wait) {
return debounce(func, wait, {
// maxWait最大的点击时间
maxWait: wait
});
}

前端开发系列129-进阶篇之Throttle And Debounce的更多相关文章

  1. openlayers5-webpack 入门开发系列一初探篇(附源码下载)

    前言 openlayers5-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载 ...

  2. leaflet-webpack 入门开发系列一初探篇(附源码下载)

    前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址 w ...

  3. 【Windows10 IoT开发系列】配置篇

    原文:[Windows10 IoT开发系列]配置篇 Windows10 For IoT是Windows 10家族的一个新星,其针对不同平台拥有不同的版本.而其最重要的一个版本是运行在Raspberry ...

  4. ESP8266开发之旅 进阶篇② 闲聊Arduino IDE For ESP8266烧录配置

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  5. 【webpack 系列】进阶篇

    本文将继续引入更多的 webpack 配置,建议先阅读[webpack 系列]基础篇的内容.如果发现文中有任何错误,请在评论区指正.本文所有代码都可在 github 找到. 打包多页应用 之前我们配置 ...

  6. iOS开发系列--Swift进阶

    概述 上一篇文章<iOS开发系列--Swift语言>中对Swift的语法特点以及它和C.ObjC等其他语言的用法区别进行了介绍.当然,这只是Swift的入门基础,但是仅仅了解这些对于使用S ...

  7. 旨在脱离后端环境的前端开发套件 - IDT Server篇

    IDT,一个基于Nodejs的,旨在脱离后端环境的前端开发套件,目的就是能让前端开发完全脱离后端的环境,无论后端是什么模板引擎(主流),都能应付自如. IDT主要包括两大部分:Server + Bui ...

  8. 前端开发【第2篇:CSS】

    鸡血 样式的属性多达几千个,但别担心,按照80-20原则,常用的也就几十个,你完全可以掌握它. Css初识 HTML的诞生 早期只有HTML的时候为了让HTML更美观一点,当时页面的开发者会把颜色写到 ...

  9. [置顶]【实用 .NET Core开发系列】- 导航篇

    前言 此系列从出发点来看,是 上个系列的续篇, 上个系列因为后面工作的原因,后面几篇没有写完,后来.NET Core出来之后,注意力就转移到了.NET Core上,所以再也就没有继续下去,此是原因之一 ...

  10. openlayers4 入门开发系列之风场图篇

    前言 openlayers4 官网的 api 文档介绍地址 openlayers4 api,里面详细的介绍 openlayers4 各个类的介绍,还有就是在线例子:openlayers4 官网在线例子 ...

随机推荐

  1. markdown常用命令行格式

    Markdown 主要命令(语法)如下: 标题 使用 # 号表示标题,# 的个数决定标题的级别: 一级标题 二级标题 三级标题 四级标题 五级标题 六级标题 段落 & 换行 直接输入文字形成段 ...

  2. iOS Facebook和Google登录

    前言 最近在对接完Google和Facebook登录之后准备对这部分内容做一个小小的总结,方便以后有需要的时候查看. 具体的Google账号申请和Facebook账号的申请在这里就不做介绍了,这部分内 ...

  3. 通过一个DEMO理解MCP(模型上下文协议)的生命周期

    在LLM应用的快速发展中,一个核心挑战始终存在:如何让模型获取最新.最准确的外部知识并有效利用工具? 背景其实很简单:大模型(LLM)再强,也总有不知道的东西,怎么办?让它"查资料" ...

  4. 100行代码实现Chat2XX(DB/ Web/ KnowledgeBase)

    这两年基于大模型的应用可谓五花八门,Chat2DB,Chat2Web,Chat2KnowledgeBase,Chat2XXX等等.本质上都是以自然语言作为系统输入,通过各种手段获取额外的上下文信息,然 ...

  5. AXUI - 极致原生体验的零依赖的国产 Web UI 框架,欢迎体验和共建!

    AXUI:专注于快速交付的国产 Web UI 框架 在日常的前端开发中,是否遇到过以下场景: 灵感乍现,希望快速通过一点代码实现原型或功能展示: 完全个人项目,开发方式自由,追求高效与便捷: 项目目标 ...

  6. 强化学习框架:OpenRLHF源码解读,模型处理

    强化学习框架:OpenRLHF源码解读,模型处理 本文主要介绍 强化学习框架:OpenRLHF源码解读,模型处理 models框架设计 了解一下 OpenRLHF的模型框架设计范式: From:htt ...

  7. Python3_模块(一)

    脚本是用 python解释器来编程,如果从 Python解释器退出再进入,那么你定义的所有的方法和变量就都消失了.为此 Python提供了一个办法,把这些定义存放在文件中,为一些脚本或者交互式的解释器 ...

  8. Python3处理文档_word文档实现自动化办公(一)

    最近打算写一个自动化出报告的脚本 先从处理word文档开始 Python 操作 Word 最常见的依赖库是:python-docx 所以,在开始操作之前,我们需要在虚拟环境下安装这个依赖库 pip3 ...

  9. MCP协议的相关知识总结

    一.基本概念与核心价值 定义与定位 MCP(Model Context Protocol,模型上下文协议)是由Anthropic推出的开放标准协议,旨在通过标准化接口实现大语言模型(LLM)与外部数据 ...

  10. DeepSeek+Coze实战:如何从0到1打造一个热点监控智能体

    大家好,我是汤师爷,专注AI智能体分享~ 短视频小白经常会遇到这样的困扰. 每天花大量时间刷视频,想要找到你所在赛道的爆款内容,却总是难以系统地整理和分析? 想要批量获取某个关键词的爆款视频数据,但是 ...