一、防抖(debounce)
1. 防抖函数原理
定义: 触发高频事件后n秒内函数只会执行一次,如果在这n秒内又被触发,则重新计时。
通俗点来讲,这就相当于用手机聊天采用手写输入法时,在你一笔一画写完一个汉字后等待一段时间,这个字就会被打印在输入框,而不是写一笔画立刻就打印一笔画。也就是说,在一段时间内连续书写笔画,并不会把笔画打印出来,而是在停笔后的一段时间,完整的汉字才会被打印。
✨✨✨✨✨✨
简单的定时器版本防抖函数(不会立即执行):
function debounce(func, delay) {
let timerId
return function () {
let context = this // 保存this指向
let args = arguments // 拿到event对象
clearTimeout(timerId)
timerId = setTimeout(function () {
func.apply(context, args)
}, delay)
}
}
这里要特别强调一下:为什么 timerId 要定义在外边? 🤔
如果放在里面,每执行函数时,就会声明一个 timerId,接着 clearTimeout 又会将这个 timerId 清掉,然后执行一个定时器,在下次再执行函数的时候,又会重新声明一个新的 timerId。
✨✨✨
也就是说这个函数一调用完,就会把它给销毁,也就是垃圾回收机制,但是下面定时器的 timerId 并没有销毁。所以我们就要将它放在外边,形成一个闭包,让这个变量私有化,的存起来形成一个单独的空间,每次改变的都是它,这样就可以清除掉定时器 timerId 了。
2. 举个例子💡
<div class="box">0</div>
<button>点我加1</button>
<script>
const btn = document.querySelector('button')
const box = document.querySelector('.box')
// 定义变量i,存放数字
let i = 1
const addFn = function () {
// 我们要让this指向box盒子,让box里的数字变化
// 在控制台打印this,方便我们观察this指向的元素是否正确
console.log(this)
this.innerHTML = i++
}
const debounce = (fn, s) => {
// 声明一个定时器标记timerId
let timerId
// 直接将function作为返回值返回
return function (...arg) {
// 每点击一次按钮,就关闭定时器
clearTimeout(timerId)
// 开启定时器
timerId = setTimeout(() => {
// 使用call()改变this指向,不改this指向的话,这里的this就不指向box了,
// addFn函数的this.innerHTML就不会执行
fn.call(this)
}, s)
}
}
// bind也是用来改变this指向的,让this指向的是显示数字的box盒子
btn.addEventListener('click', debounce(addFn, 1000).bind(box))
</script>
效果如下:
从以上例子中可以看出,当我在一段时间内连续点击时,数字并不会加1,在我停止点击后一段时间,数字才会加1,这就是防抖。
同时,这个简单的例子中也涉及到了很多的小细节,下面我会一一为大家讲解。
2.1 return分析🤔
因为在监听事件中调用的 debounce 加了一个括号,所以它会立即执行,所以上面的 debounce 里面的代码会立即执行,当我们再点击按钮的时候,就没有效果了。
✨✨✨
如果我们 return 一个函数,就相当于把那个函数拿到了事件监听的第二个参数里面,触发事件的时候才会执行。
✨✨✨
也就是 return function
外面的代码,绑定 click 事件的时候,就立即执行了;return function
里面的代码,触发事件的时候执行。
2.2 this指向👉
因为调用的 addfn 函数会在定时器里运行,且定时器里的 this 指向的是window,而函数在没有被调用之前 this 的指向是 box,因此这样就改变了 this 的指向,addFn 函数的 this.innerHTML
就不会执行。
所以,我们需要改变 this 的指向,使其一直指向 box。
this 指向的补充知识点:
- 箭头函数的 this 在定义的时候就确定了,指向的是上层作用域中的 this
- 全局作用域中 / 普通函数中 / 定时器里面 this 指向 window
- 事件注册的时候, this指向被绑定的元素
- 构造函数中, this 指向的是 构造函数的实例
2.3 改变this指向👉
先介绍三种改变this指向的方法:call、apply、bind。
✨✨✨
call: 改变this的指向;会调用函数,且立即执行;第一个参数是this指向的对象,第二个参数是参数列表。
apply: 改变this的指向;会调用函数,且立即执行;第一个参数是this指向的对象,第二个参数是数组。
bind: 改变this指向;不会立即执行,需要我们手动调用执行;有返回值,返回值是一个函数,
call、apply、bind 的区别:
- 都可以改变this指向
- call接收的是参数列表,apply接收的是数组
- call和apply会立即执行,bind返回函数,需要手动调用
2.4 传参🚙
有时我们会用到更多的参数,如果都写进去有点多,所以我们可以用剩余参数来传参。这个时候在fn.call(this,…args)
里面它(...args)叫扩展运算符,也可以写成 fn.apply(this,args)
。
二、节流(throttle)
1. 节流函数原理
定义: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效。
节流通俗点来讲,就是在抢优惠券的时候,无论你重复点击它多少次,只会生效一次。
✨✨✨✨✨✨
时间戳版本的节流函数:(立即执行)
function throttled1(fn, delay) {
let oldtime = Date.now()
return function (...args) {
let newtime = Date.now()
if (newtime - oldtime >= delay) {
fn.apply(null, args)
oldtime = Date.now()
}
}
}
当时间戳now与时间戳起点的时间间隔大于delay时,执行回调函数。最后将当前时间记录下来,作为下一次计时的起点。
2. 举个例子💡
<div class="box">0</div>
<button>点我加1</button>
<script>
// 获取按钮和显示框
const btn = document.querySelector('button')
const box = document.querySelector('.box')
// 定义变量i,存放数字
let i = 1
const addFn = function () {
// 我们要让this指向box盒子,让box里的数字变化
// 在控制台打印this,方便我们观察this指向的元素是否正确
console.log(this)
this.innerHTML = i++
}
const throttle = (fn, s = 0) => {
// 定义时间戳起点
let pre = 0
return function () {
// 鼠标一点击按钮,就记录下当前的时间戳
let now = Date.now()
// 当时间戳now与时间戳起点的时间间隔大于s时,执行回调函数
if (now - pre >= s) {
// 绑定this,确保this的指向正确
fn.apply(this)
// 将当前时间记录下来,作为下一次计时的起点
pre = Date.now()
}
}
}
// 同时使用bind()函数让this指向box
btn.addEventListener('click', throttle(addFn, 3000).bind(box))
</script>
效果如下:
从以上例子我们可看出,在2s内无论我点击多少次,数字都只会加1,也就是只执行一次,这就是节流。
✨✨✨
此代码段中也涉及到了 return 的分析、this 指向和传参的问题,这里就不再重复说明了。
三、区别和应用场景
1. 相同点🔎
- 都可以通过使用 setTimeout 实现
- 目的都是,降低回调执行频率。节省计算资源
2. 不同点🔎
- 函数防抖,在一段连续操作结束后,处理回调,利用 clearTimeout 和 setTimeout 实现。函数 节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。
- 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次。
3. 应用场景🏠
防抖: 频繁触发按钮点击事件、input 框搜索等。
节流: 浏览器窗口缩放 resize 事件,滚动 scroll 事件,mousemove 事件等。