JavaScript防抖与节流手写实现详解
本文深入解析JavaScript中防抖(debounce)与节流(throttle)的核心概念,通过电梯与游戏技能的生动比喻帮助理解。详细演示了如何利用setTimeout、clearTimeout及闭包机制手写实现这两个功能,并解决了this指向与参数传递问题。最后总结了搜索框、滚动加载等典型应用场景,助力前端性能优化。
前言
回顾之前阅读的大佬文章《一杯茶的时间,带你彻底学会手写防抖节流》,虽然当时对防抖节流的概念有了初步清晰的认识,但由于是初次接触,对于具体的手写实现逻辑仍感到困惑,并在此处停留了较长时间。
因此,本文旨在基于前述文章进行补充说明,帮助初学者真正掌握防抖与节流的手写技巧。
防抖节流的概念
- 防抖:在 n 秒后执行事件,若在 n 秒内被重复触发,则重新计时。
- 节流:在 n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效。
一个经典的比喻:
想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应
假设电梯有两种运行策略 debounce 和 throttle,超时设定为15秒,不考虑容量限制
电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖。
电梯第一个人进来后,15秒后准时运送一次,这是节流。
再举个不太严谨的比喻:
防抖:就是王者荣耀里的回城,一段时间内被打断,就又要重新计时。
节流:就是王者荣耀里英雄的技能,像东皇的大一般都是20s准时来一次。
手写
防抖
首先我们先简单的模拟一个按钮被点击的过程。
let addBtn=document.getElementById('add')
function addOne(){
console.log('增加一个')
}
addBtn.addEventListener('click',addOne)加入防抖功能
// debounce.js
let addBtn=document.getElementById('add')
function addOne(){
console.log('增加一个')
}
function debounce(fun,time){
setTimeout(()=>{
fun()
},time)
}
addBtn.addEventListener('click',debounce(addOne,2000))
现在延时的目的是达到了但是每次点击都会新增一个新的setTimeout而且并不能达到我们多次点击只执行一次的效果。
这时候就需要clearTimeout登场了,我们需要在我们点击了按钮后也就是debounce执行时要先把之前的setTimeout先清除再重新计时。
这时我们会很自然地想到这个写法:
function debounce(fun, time) {
let timer;
// 如果之前就存在定时器,就要把之前那个定时器删除
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fun()
}, time)
}但是这样写的话拿的到之前定时器的值吗?
当然是拿不到的,因为每次都会重新let timer,所以这个时候我们就要使用闭包(延迟了变量的生命周期)了。
如果你还不太理解闭包,可以参考这篇文章包教包会——作用域链+闭包
引入闭包后
function addOne(){
console.log('增加一个')
}
function debounce(fun, time) {
let timer;
return function(){
// 如果之前就存在定时器,就要把之前那个定时器删除
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fun()
}, time)
}
}
addBtn.addEventListener('click',debounce(addOne,2000))
这里还有一点需要知道的,每执行一次addEventListener,debounce因为返回的是一个函数,并结合addEventListener的特点,会直接执行debounce返回的函数,不会出现每次都let timer
- 现在我们的一个防抖功能就完成了,但是这还没完,如果我们在
addOne()打印this会发现我们这样执行的this是指向Window的。 - 这当然不是我们所希望的,我们需要使用
apply来改变this指向,再者就是我们需要考虑到执行函数的参数,因为不同的函数肯定会有不同的参数传入,对于参数我们可以使用arguments处理。
function debounce(fun, time) {
let timer;
return function(){
// 如果之前就存在定时器,就要把之前那个定时器删除
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fun.apply(this, arguments)
}, time)
}
}这样一个防抖就写出来了
节流
- 首先我们先模拟一个触发事件。
- 接下来我们封装一个节流函数,跟防抖一样我们也需要利用闭包,顺便再加一个参数接收节流时间。
// throttle.js
function scrollTest(){
console.log('现在我触发了')
}
function throttle(fun,time){
return function(){
fun()
}
}
document.addEventListener('scroll',throttle(scrollTest,3000))
这里用闭包是为了后面获得上一次的时间
- 因为我们的节流是在一段时间内执行一次也就是说如果两次鼠标滚动的时间间隔未到所设置的时间则不执行。
- 那我们可以记录一下每次滚动的时间戳来进行对比。
- 当然我们也需要像防抖一样改变
this指向和接收参数,最后完成后是这样的。
// throttle.js
...
function throttle(fun,time){
let t1=0 //初始时间
return function(){
let t2=new Date() //当前时间
if(t2-t1>time){
fun.apply(this,arguments)
t1=t2
}
}
}应用场景
防抖在连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小
resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流在间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 搜索框,搜索联想功能
到此这篇关于一文学会JavaScript如何手写防抖节流的文章就介绍到这了,更多相关JavaScript防抖节流内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
