JS 的节流与防抖
一、函数节流:一定时间间隔内,第一次有效
是指规定一个时间间隔,在这个时间间隔内,只能有一次触发事件的回调函数执行,如果在时间间隔内事件被触发多次,则不会执行回调。
应用场景:可以将一些事件降低触发频率,比如懒加载时要监听计算滚动条的位置,点击抽奖、抢购
按照定义,咱们先不考虑多的,按小白的理解写一个节流函数,以 Vue
为例:
- 假设我们使用【节流函数】时,该函数是被包裹在我们给按钮绑定的
clickMe
方法里面的 - 声明一个事件状态变量
timer
、声明防抖函数throttle
- 防抖函数接受两个参数,一个是传递的本该执行的方法
cb
,一个是防抖的时间间隔delay
- 进入方法,如果
timer
为真,则return
,不执接下来的逻辑 - 如果
timer
为null
,则开启一个定时器,并且赋值给timer
,定时器的等候时间即为delay
参数,定时器执行后,清除计时器,并且将timer
恢复为初始值null
- 最后执行传递的方法
cb
- 这样当我们第一次点击按钮时,
timer
为null
,执行计时器,执行cb
,之后在1s
内再次点击的话,timer
为真,则不会执行之后的逻辑,1s
过后,再次点击,timer
为null
,则又可以执行了
<template>
<button @click="clickMe('Jerry', 'Tom')">点我吧!</button>
</template>
<script>
// 节流
// 2、全局声明一个事件状态变量、声明防抖函数
let timer = null
// 3、两个参数:一个是传递的方法,一个是防抖的时间间隔
function throttle (cb, delay) {
// 4、进入方法,如果timer为真,则return,不执接下来的逻辑
if (timer) return
// 5、否则则开启一个定时器,并且赋值给timer,定时器的等候时间即为delay参数,定时器执行后,清除计时器,并且将timer恢复为初始值null
timer = setTimeout(() => {
clearTimeout(timer)
timer = null
}, delay)
// 执行传递的方法cb
return cb()
}
export default {
methods: {
clickMe (name, name2) {
// 1、假设我们使用【节流函数】时,是被包裹在我们给按钮绑定的clickMe 方法里面的
throttle(() => {
console.log('点了!', name)
console.log('点了!', name2)
}, 1000)
}
}
}
</script>
复制代码
但是,这种实现方式是有问题的,因为 timer
是被定义在【全局作用域】的,所以用闭包(有权访问另一个函数作用域变量的函数,称为闭包)的形式改进:
- 现在
throttle
直接绑定在clickMe
上, - 当前组件初始化的时候,
clickMe
绑定的throttle
会被执行一次,throttle
会将我们传递的cb
方法进行包装,再return
一个新的方法给clickMe
- 此时
timer
的作用域也被限制在了throttle
中
<template>
<button @click="clickMe('Jerry', 'Tom')">点我吧!</button>
</template>
<script>
// 节流
function throttle (cb, delay) {
// 计时器
let timer
// 调用 throttle 后,会将 cb 进行包装、执行并返回给调用者
return function () {
// 如果计时器存在,则阻止用户操作
if (timer) return
// 否则开启一个计时器
timer = setTimeout(() => {
// 时间间隔结束,结束计时器
clearTimeout(timer)
// 将 timer 初始化
timer = null
}, delay)
// 执行回调
cb(...arguments)
}
}
export default {
methods: {
// 现在throttle直接绑定在clickMe上,throttle 会 return一个新的方法
clickMe: throttle((name, name2) => {
console.log('点了!', name)
console.log('点了!', name2)
console.log(this) // undefined
}, 1000)
}
}
</script>
复制代码
此处几个知识点:
arguments
和 剩余参数
- 在普通函数
function
中,要想获得所有参数对象,则是通过关键字arguments
可以获取 - 在箭头函数中,没有
arguments
,则可以通过剩余参数(...args)
获取
但是,我们现在方法中获取 this
的话,是获取不到的,所以我们在执行 cb(...arguments)
需要修改 cb
内部 this
指向,有三种方法:
2. 修改 this
指向
cb.call(this, ...arguments)
cb.apply(this, arguments)
cb.bind(this)(...arguments)
这样,我们就能正确的获取 this
指向了。
最后我们对代码在进行一下优化,相较于计时器,我们可以用 Date.now()
来替代,性能会更好,以下就是完整代码:
<template>
<button @click="clickMe('Jerry', 'Tom')">点我吧!</button>
</template>
<script>
// 节流
function throttle (cb, delay) {
// 上次操作的时间
let lastTime = 0
return function () {
const triggerTime = Date.now()
// 当前操作时间 - 上次操作时间 > 时间间隔,才执行
if (triggerTime - lastTime > delay) {
cb.call(this, ...arguments)
lastTime = triggerTime
}
}
}
export default {
methods: {
clickMe: throttle(function (name, name2) {
console.log('点了!', name)
console.log('点了!', name2)
console.log('点了!', this)
}, 1000)
}
}
</script>
复制代码
二、函数防抖:一定时间间隔内,最后一次有效
是指规定一个时间间隔,在事件被触发该时间间隔后再执行回调,如果在这个时间间隔内事件又被触发,则重新计时,不执行回调。
应用场景:搜索框(当用户输入内容后,n秒后没有再次输入,就搜索)、监听窗口放大缩小resize、滚动监听
function debounce(cb, delay = 500) {
// 计时器
let timer = null
return function (...args) {
// 如果计时器存在,则清除计时器,重新计时(如果用户一直在输入,则重置计时器)
if (timer) clearTimeout(timer)
// 开启计时器(delay时间后,用户没有输入,则出发搜索)
timer = setTimeout(() => {
// 到达时间间隔,执行回调
cb.call(this, args)
}, delay)
}
}
复制代码
使用:在 onScorll 中使用防抖
// 用debounce来包装scroll的回调
const scroll = debounce(() => {
console.log("触发了滚动事件")
}, 1000)
document.addEventListener("scroll", scroll)
复制代码
三、throttlePlus:使用 debounce 来优化 throttle
throttle
的问题是它太有耐心了,假设:
- 首先我们将
delay
设置为3s
,当用户第一次点击后,会执行cb
- 紧接着,用户狂点,但是并未等到
3s
这个时间节点到达之前,就停止了点击 - 那么除了用户第一次的点击会执行
cb
之外,后面的这些点击都不会执行cb
- 这样就会导致用户的操作一直没有得到反馈,对这个页面产生“卡死”了的感觉。
所以,为了解决这样的一个问题,我们需要将 throttle
和 debounce
结合起来,若用户在 3s
之前结束了操作,那么计时器到了 3s
这个时刻,就执行一次 cb
扫描二维码关注公众号,回复:
13703609 查看本文章
function throttlePlus (cb, delay) {
// 上次操作的时间
let lastTime = 0
// 计时器
let timer = null
return function () {
/** @不在时间间隔内 **/
// 当前操作的时间
const triggerTime = Date.now()
// 如果 当前操作的时间 - 上次操作的时间 > 时间间隔,则执行 cb
if (triggerTime - lastTime > delay) {
lastTime = triggerTime
cb.call(this, ...arguments)
} else {
/** @在时间间隔内 **/
// 用户若一直操作,则重置计时器
timer && clearTimeout(timer)
// 设置定时器,到达我们设置的 delay 间隔,就给用户执行一次
timer = setTimeout(() => {
lastTime = triggerTime
clearTimeout(timer)
cb.call(this, ...arguments)
}, delay)
}
}
}
复制代码
使用:在 onScorll 中使用加强版 throttlePlus
// 用throttlePlus来包装scroll的回调
const scroll = throttlePlus(() => {
console.log("触发了滚动事件")
}, 1000)
document.addEventListener("scroll", scroll)
复制代码