前端开发系列129-进阶篇之Throttle And Debounce
在前端开发中我们可能经常需要给(页面)标签绑定一些持续触发的事件,如 resize、scroll、input、mousemove、keyup和keydown 等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。
譬如,如果用户有频繁的resize 和 scroll 行为,那么会导致页面不断的被重新渲染,如果在绑定的回调函数中存在大量的 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, 此外在事件处理函数中可能还会存在事件对象等参数的传递,需要考虑到这种情况。
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的更多相关文章
- openlayers5-webpack 入门开发系列一初探篇(附源码下载)
前言 openlayers5-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载 ...
- leaflet-webpack 入门开发系列一初探篇(附源码下载)
前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址 w ...
- 【Windows10 IoT开发系列】配置篇
原文:[Windows10 IoT开发系列]配置篇 Windows10 For IoT是Windows 10家族的一个新星,其针对不同平台拥有不同的版本.而其最重要的一个版本是运行在Raspberry ...
- ESP8266开发之旅 进阶篇② 闲聊Arduino IDE For ESP8266烧录配置
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
- 【webpack 系列】进阶篇
本文将继续引入更多的 webpack 配置,建议先阅读[webpack 系列]基础篇的内容.如果发现文中有任何错误,请在评论区指正.本文所有代码都可在 github 找到. 打包多页应用 之前我们配置 ...
- iOS开发系列--Swift进阶
概述 上一篇文章<iOS开发系列--Swift语言>中对Swift的语法特点以及它和C.ObjC等其他语言的用法区别进行了介绍.当然,这只是Swift的入门基础,但是仅仅了解这些对于使用S ...
- 旨在脱离后端环境的前端开发套件 - IDT Server篇
IDT,一个基于Nodejs的,旨在脱离后端环境的前端开发套件,目的就是能让前端开发完全脱离后端的环境,无论后端是什么模板引擎(主流),都能应付自如. IDT主要包括两大部分:Server + Bui ...
- 前端开发【第2篇:CSS】
鸡血 样式的属性多达几千个,但别担心,按照80-20原则,常用的也就几十个,你完全可以掌握它. Css初识 HTML的诞生 早期只有HTML的时候为了让HTML更美观一点,当时页面的开发者会把颜色写到 ...
- [置顶]【实用 .NET Core开发系列】- 导航篇
前言 此系列从出发点来看,是 上个系列的续篇, 上个系列因为后面工作的原因,后面几篇没有写完,后来.NET Core出来之后,注意力就转移到了.NET Core上,所以再也就没有继续下去,此是原因之一 ...
- openlayers4 入门开发系列之风场图篇
前言 openlayers4 官网的 api 文档介绍地址 openlayers4 api,里面详细的介绍 openlayers4 各个类的介绍,还有就是在线例子:openlayers4 官网在线例子 ...
随机推荐
- AIR724UG上电后引脚的默认电平
使用AIR724UG的引脚作为继电器的控制引脚时,由于上电时引脚的电平有的高,有的低. 在某些场合我们希望GPIO上电默认是低电平,所以我将常用引脚中可用的引脚标注了出来.
- Redis 集群实现分布式缓存的示例操作流程【Redis 系列之五】
〇.前言 Redis 集群的核心优势在于高可用性.可扩展性和高性能,特别适合需要处理大规模数据和高并发请求的应用场景. 本文先介绍了什么是 Redis 集群,然后通过示例,以手动和自动两种方式搭建集群 ...
- 🎀Excel-多表数据查找匹配(VLOOKUP)
简介 Excel的VLOOKUP函数同样可以用来查找表格中的数据.VLOOKUP(垂直查找)是一个非常有用的函数,它可以在一个表格或数据表的一列中搜索特定的值,并返回与之在同一行上的另一列中的值. 环 ...
- C# 拓展方法( 二)——模拟拓展方法的场景
namespace ExpandingMethodDemo{ class Program { static void Main(string[] args) { Phone ph = new Phon ...
- CS及免杀
Strategic Cyber 责任有限公司发行了适用于 Windows.Linux 和 MacOS X 的 Cobalt Strike 软件包. 要安装 Cobalt Strike,只需将其存档解压 ...
- 基于Kubernetes可扩展的Selenium 并行自动化测试部署及搭建(2)——Win10环境下Kubernetes(k8s)部署
继续上一篇,本篇进行K8S环境部署. K8s部署: 1. 访问k8s-for-docker-desktop 的github地址: https://github.com/AliyunContainer ...
- ufw配置自动管理端口转发和DNAT+MASQUERADE
端口A转发到本地的端口B 端口A转发到另一台机器的端口B(需借助DNAT) 一般情况下, 我们配置ufw来实现端口转发时会在修改 /etc/ufw/before.rules 文件, 增加*nat部分. ...
- 盈亏平衡之"盈亏平衡点和总可变成本和总收入和利润和边际收益"
案例1 案例2 案例3 因为他这里没有按2w件来算,而是按4w件 利润 = 总收入 - 总成本 总收入 = 产量 * 单价 总成本 = 固定成本 + 变动成本 变动成本 = 单件可变成本 * 产量 案 ...
- 在rk3588上部署InternVL系列
在rk3588上部署InternVL2-1B 准备 首先要在hf上下载InternVL2-1B的模型传送门(镜像) git clone https://hf-mirror.com/OpenGVLab/ ...
- 妙妙线段树+DFS序判断子孙节点,但似乎还可以树链剖分?(CF Div3 909 G)
G. Unusual Entertainment 原题链接:https://codeforces.com/contest/1899/problem/G 题目大意: 给定一棵树,根节点为1,给定一个\( ...