当前位置:网站首页>2000字助你精通防抖与节流
2000字助你精通防抖与节流
2022-07-26 19:15:00 【前端开发小司机】
前言
防抖与节流是老生常谈的话题了,不管是在面试还是实际开发中都经常涉及。
本文将介绍防抖与节流的概念、应用场景、代码实现,其中代码实现参考了 lodash 的源码,剔除了其中参数类型与运行环境的检测,使代码更简洁易懂,方便学习理解。
概念与应用
防抖与节流都是为了避免代码被频繁执行
防抖
防抖(debounce):在下达指令后会开始计时,如果在计时范围内又重复下达指令,就重新计时,等待计时完成后才执行代码。
打个比方:公交车进站,行人陆续上车,假设等待时间是20秒,那就只有持续20秒无人上车时,公交车才会开走(执行代码)。
节流
节流(throttle):在代码执行后进入冷却,冷却期间不会重复执行,冷却到了才会再次执行。
打个比方:女神每天回复舔狗一次,今天回复过后即便舔狗发再多信息,女神也只会等到第二天才回复(再次执行代码)。
应用场景
在说应用场景之前,先统一防抖和节流的概念
节流是包含最大时限的防抖
解释一下:假设100秒内持续频繁下达指令,防抖的处理结果就是100秒后才会执行,但这样对用户极不友好的。往往会在防抖代码中加一个最大时限,当达到最大时限时,即便仍然在等待时间内下达指令,但代码也会执行一次。这就变成了节流。
防抖与节流一般应用于 搜索提示、页面滚动等
也用来限制那些频繁触发又不确定次数的事件:mousemove、scroll、resize
目前浏览器性能过剩,为了用户良好的体验,以上这些场景基本都采用了节流。
代码实现
在上面也说过了,防抖与节流的区别就在于是否具有最大时限
所以在实现方面,先实现防抖函数,在后续加上最大时限来使其变为节流函数
在写代码前,要明确要实现的函数的参数、返回值
- 函数需要传入两个参数,分别为想要限制执行频率的函数与延迟时间(单位为毫秒)
- 函数的返回值也是一个函数,是与传入函数功能相同,已经防抖动的函数
由于在实现期间需要涉及到三个函数,为了避免混乱,在此统一一下称呼:
- 我们即将要实现的函数,命名为
debounce,下文称作外部函数 - 已经防抖动的函数,也就是外部函数的返回值,命名为
debounced,下文称作防抖函数 - 需要防抖动的函数,也就是用户调用外部函数时传入的函数,命名为
func,下文称作内部函数
而对于内部函数,由于其函数并不会立刻执行,也就不应该具有返回值
认识 requestAnimationFrame
基于 setTimeout 实现的防抖/节流函数网上已有很多,lodash 源码中使用的是 requestAnimationFrame,其性能与稳定性都要优于 setTimeout,所以本文也基于 requestAnimationFrame 实现
requestAnimationFrame() 需要传入一个函数作为参数,该函数会在浏览器下一次重绘之前执行
浏览器的重绘频率是每秒 60 次,约 16ms 重绘一次。
可以简单的将 requestAnimationFrame 函数视为延迟为16ms 的 setTimeout 函数
防抖函数实现
防抖函数代码实现如下,每次调用防抖函数都会重新计时,由于是基于 requestAnimationFrame,需要递归开启计时器
/**
* @description:
* @param {Function} func 要防抖的函数,内部函数
* @param {number} wait 等待时间
* @return {Function} 已防抖的函数
*/
function debounce(func, wait) {let lastArgs, // 保存参数lastThis, // 保存thistimerId, // 定时器idlastCallTime // 最近调用防抖函数的时间// 重置定时器function startTimer(pendingFunc) {// 取消掉上一次开启的定时器cancelAnimationFrame(timerId)// 开启新的定时器并返回定时器idreturn requestAnimationFrame(pendingFunc)}// 检测是否到了该执行的时间function shouldInvoke(time) {const timeSinceLastCall = time - lastCallTime// 距离上一次防抖函数的调用已超过等待时间return timeSinceLastCall >= wait}// 调用内部函数function invokeFunc() {// 获取之前保存的this与参数const args = lastArgsconst thisArg = lastThis// this与参数置空,不影响垃圾回收lastArgs = lastThis = undefinedfunc.apply(thisArg, args)}// 传入定时器的回调函数// 不断获取当前时间判断是否应该调用内部函数function timerExpired() {const time = Date.now()if (shouldInvoke(time)) {// 执行内部函数timerId = undefinedinvokeFunc()} else {// 递归开启定时器timerId = startTimer(timerExpired)}}// 返回的防抖函数,该函数无返回值function debounced(...args) {const time = Date.now()// 更新this与参数lastArgs = argslastThis = this// 更新防抖函数调用的时间lastCallTime = time// 开启定时器timerId = startTimer(timerExpired)}return debounced
}
// 测试功能
let preTime = Date.now()
const func = () => {let nextTime = Date.now()console.log(nextTime - preTime)preTime = nextTime
}
const dfunc = debounce(func, 50)
dfunc()
dfunc()
// 经过50ms后,控制台打印50
setTimeout(() => {dfunc()dfunc()
}, 100)
// 经过150ms后,控制台打印100
节流函数实现
在 lodash 中,节流函数与防抖函数公用一套代码,只是配置参数不同,本文也将复用之前的代码
节流函数比防抖函数多两个特点
- 节流函数包含最大时限(maxWait),这是防抖函数与节流函数的区别
- 节流函数往往会立刻执行内部函数一次(leading)
完整代码如下:
/**
* @description:
* @param {Function} func 要防抖的函数,内部函数
* @param {number} wait 等待时间
* @param {number|undefined} maxWait 最大时限,有的话是节流函数,没有的话是防抖函数
* @param {boolean} leading 规定在延迟开始前是否调用内部函数,默认不调用
* @return {Function}
*/
function debounce(func, wait, maxWait = undefined, leading = false) {let lastArgs, // 保存参数lastThis, // 保存thistimerId, // 定时器idlastCallTime, // 最近调用防抖函数的时间lastInvokeTime // 最近调用内部函数的时间,0初值确保let maxing = !!maxWait // 是否指定了最大等待时间// 最大时限不应该小于等待时间if (maxWait) {maxWait = Math.max(wait, maxWait)}// 重置定时器function startTimer(pendingFunc) {cancelAnimationFrame(timerId)return requestAnimationFrame(pendingFunc)}// 检测是否到了该执行的时间function shouldInvoke(time) {const timeSinceLastCall = time - lastCallTimeconst timeSinceLastInvoke = time - lastInvokeTime// 上次内部函数时间尚未定义 (首次执行节流函数)// 距离上一次防抖函数的调用已超过等待时间 (防抖函数的功能)// 设置了最大时限,且距离上次内部函数的调用已达到最大时限 (节流函数的功能)return (lastInvokeTime === undefined ||timeSinceLastCall >= wait ||(maxing && timeSinceLastInvoke >= maxWait))}// 调用内部函数function invokeFunc(time) {// 获取之前保存的this与参数const args = lastArgsconst thisArg = lastThis// this与参数置空,不影响垃圾回收lastArgs = lastThis = undefined// 更新最近内部函数的调用时间lastInvokeTime = timefunc.apply(thisArg, args)}// 不断获取当前时间判断是否应该调用内部函数function timerExpired() {const time = Date.now()if (shouldInvoke(time)) {timerId = undefined// 如果已经立刻执行内部函数// 且等待时间内没有再次调用节流函数的话// 就不需要在等待时间过后再次执行内部函数了if (lastArgs) invokeFunc(time)} else {// 重新开启定时器timerId = startTimer(timerExpired)}}// 返回的防抖函数,该函数无返回值function debounced(...args) {const time = Date.now()// 这里检测是否应该重置定时器const isInvoking = shouldInvoke(time)// 更新this与参数lastArgs = argslastThis = this// 更新防抖函数调用的时间lastCallTime = timeif (isInvoking) {if (timerId === undefined) {// 首次执行节流函数,更新内部函数调用时间lastInvokeTime = timetimerId = startTimer(timerExpired)// 检测leading属性,立即调用内部函数if (leading) invokeFunc(time)} else if (maxing) {// 节流功能,执行内部函数并重置定时器timerId = startTimer(timerExpired)invokeFunc(time)}} else if (timerId === undefined) {// 内部函数刚执行完又调用了节流函数// 只开启定时器,无需更新内部函数调用时间timerId = startTimer(timerExpired)}}return debounced
}
/**
* @description:
* @param {Function} func 要防抖的函数,内部函数
* @param {number} wait 冷却时间
* @param {boolean} leading 规定在延迟开始前是否调用内部函数,默认调用
* @return {Function}
*/
function throttle(func, wait, leading = true) {return debounce(func, wait, wait, leading)
}
// 测试功能
let preTime = Date.now()
let arr = []
const func = () => {let nextTime = Date.now()arr.push(nextTime - preTime)preTime = nextTime
}
const tfunc = throttle(func, 100, false)
let id = setInterval(tfunc, 10)
setTimeout(() => {clearInterval(id)console.log(arr) // [112, 101, 104, 103, 100, 100, 100, 101, 106, 101]
}, 1000)
其他功能实现
来看一个业务场景吧 鼠标悬停在按钮上 0.5 秒后出现按钮的功能提示,有多个按钮,只显示最后鼠标悬停的按钮的功能提示,很明显要用防抖来实现
然而如果用户在 0.5 秒内从按钮移出,理应不显示提示,但防抖函数的定时器已经设置,0.5 秒后提示依旧显示,很明显的 bug
所以,防抖函数身上应该有个取消定时器的功能
function debounce(func, wait, maxWait = undefined, leading = false) {……debounced.cancel = function () {// 清除定时器if (timerId !== undefined) {cancelAnimationFrame(timerId)}// 清空变量lastArgs = lastThis = timerId = lastCallTime = lastInvokeTime = undefined}return debounced
}
结语
相信经过本文的阅读,您对防抖与节流一定有较深的理解了
本文代码实现只提取了 lodash 源码中核心的部分,且做了一定的修改
lodash 提供的其他配置项与功能在业务中极少使用,本文便不做介绍了,有兴趣的可以自行去查看
如果文中有不理解或不严谨的地方,欢迎评论提问。
如果喜欢或有所帮助,希望能点赞关注,鼓励一下作者。
边栏推荐
- ShardingSphere-JDBC 关键字问题
- Zabbix调用api检索方法
- 以 NFT 为抵押品的借贷协议模式探讨
- 3万脱发人,撑起一个IPO
- [internship experience] date verification
- [internship experience] exception handling and URL result response data processing
- Leetcode daily practice - 189. Rotation array
- Is it safe for CSCI qiniu school to open an account? What is qiniu for
- Use of load balancing
- Kingbasees SQL language reference manual of Jincang database (17. SQL statement: discard to drop language)
猜你喜欢

u盘损坏怎么恢复原来数据,u盘损坏数据如何恢复

Software testing - what are the automated testing frameworks?
![[internship experience] exception handling and URL result response data processing](/img/ed/05622fad0d3d8dcf17ce7069340669.jpg)
[internship experience] exception handling and URL result response data processing

一文看懂中国的金融体系

How to compress the traffic consumption of APP under mobile network in IM development
![[PHP] save session data to redis](/img/29/70a9f330b9f912ccbd57e865372439.png)
[PHP] save session data to redis

企业数字化转型成大趋势,选对在线协作工具很重要

How to uninstall win11 edge? The method tutorial of completely uninstalling win11 edge browser

How to adjust the abnormal win11 USB drive to normal?

网络与VPC动手实验
随机推荐
Live video source code to achieve the advertising effect of scrolling up and down
金仓数据库 KingbaseES SQL 语言参考手册 (13. SQL语句:ALTER SYNONYM 到 COMMENT)
Solution to the third game of 2022 Niuke multi school league
LeetCode每日一练 —— 26. 删除有序数组中的重复项
【实习经验】日期校验
数据库设计三大范式
LeetCode每日一练 —— 88. 合并两个有序数组
【ffmpeg】给视频文件添加时间戳 汇总
What is a knowledge management system? You need to know this
【OBS】Dropped Frames And General Connection Issues
京东荣获中国智能科学技术最高奖!盘点京东体系智能技术
【MySQL】 - 索引原理与使用
iPhone开发 数据持久化总结(终结篇)—5种数据持久化方法对比
Deeply analyze the execution process of worker threads in the thread pool through the source code
Leetcode daily practice - 27. Remove elements
BluePrism流程业务对象的组件功能介绍-RPA第三章
使用ECS和OSS搭建个人网盘
win11 edge怎么卸载?win11 edge浏览器彻底卸载的方法教程
罗永浩赌上最后一次创业信用
操作系统常见面试题目总结,含答案