防抖/节流函数important
防抖和节流可视化指南
当前对于前端而言 JavaScript 更多的是运行在浏览器平台中,其中有很多人机交互的行为。比如:可左右切换的轮播图,当短时间内多次重复点击切换按钮时,会频繁触发该按钮绑定的事件监听进而执行对应的处理函数。而浏览器自身也有监听机制,在一定的时间范围内(Chrome 大概是 4 ~ 6ms)如果监听到了某个事件被触发,就会去执行事件对应的处理函数。而函数的执行会占用内存空间,浏览器自身作为一个应用,所占用的内存也是有限的。因此在一些高频次事件触发的场景下,不希望对应的事件处理函数立即或者多次执行。这就是需要使用防抖和节流函数的原因。
防抖函数
对于高频操作,只希望识别一次点击,可以是第一次也可以是最后一次
应用场景
- 滚动事件
- 输入的模糊匹配
- 轮播图切换
- 点击事件
- 。。。
前置场景
页面上有一个按钮,可以连续多次点击。
<button id="btn">点击</button>
添加点击事件
const btn = document.getElementById('btn')
function btnClick() {
console.log('点击了按钮')
}
btn.onclick = btnClick
此时并没有防抖效果
实现防抖函数
1、防抖函数就是在我们要执行的函数外层包裹一个函数,因此它应该返回一个函数:
function debounce(handle) {
return function proxy() {
handle()
}
}
handle 就是实际要执行的函数,比如点击事件绑定的处理函数。
调用方式需要修改为:
btn.onclick = debounce(btnClick, 300, false)
2、参数类型判断及默认值处理
- handle 必须是一个函数,否则就抛出错误。
- 除了 handle 之外,还需要有一个 wait 参数表示事件触发多久之后开始执行。以及一个 immediate 参数控制执行第一次还是最后一次。
/**
* handle 需要执行的事件监听
* wait 事件触发后多久开始执行
* immediate 控制执行第一次还是最后一次,false:最后一次,true:第一次
*/
function debounce(handle, wait, immediate) {
if (typeof handle !== 'function') {
throw new Error('handle must be an function')
}
if (typeof wait === 'boolean') {
immediate = wait
wait = 300
}
if (typeof wait !== 'number') {
wait = 300
}
if (typeof immediate !== 'boolean') {
immediate = false
}
// ...
}
3、要实现控制 handle 的执行时机和次数,就需要用到 setTimeout 进行延迟执行
function debounce(handle, wait, immediate) {
// ...
return function proxy() {
setTimeout(() => {
handle()
}, wait)
}
}
4、参数传递
handle 处理函数中的参数以及 this,在调用 debounce 的时候也要能获取到。
function debounce(handle, wait, immediate) {
// ...
let timer = null
return function proxy(...args) {
const self = this
clearTimeout(timer)
timer = setTimeout(() => {
handle.call(self, ...args)
}, wait)
}
}
如下图,可以获取到 this 值以及点击事件的事件对象 event
5、执行最后一次(immediate=false)
如果想要执行最后一次,就意味着无论点了多少次,前面的 n-1 次都无用。此时只需要使用 clearTimeout()将之前每一次点击生成的定时器清除,这样就只保留了最后一次定时器,达到了只执行最后一次的目的。
function debounce(handle, wait, immediate) {
// ...
let timer = null
return function proxy(...args) {
const self = this
clearTimeout(timer)
timer = setTimeout(() => {
handle.call(self, ...args)
}, wait)
}
}
可以看到,当最后一次点击停止时,才执行了点击事件绑定的处理函数。
试一试
Count is:
6、执行第一次(immediate=true)
如果要执行第一次的话,就需要在 proxy 中立即执行 handle,而且 setTimeout 中不能执行。
function debounce(handle, wait, immediate) {
// ...
let timer = null
return function proxy(...args) {
const self = this
const init = immediate
clearTimeout(timer)
timer = setTimeout(() => {
!init ? handle.call(self, ...args) : null
}, wait)
init ? handle.call(self, ...args) : null
}
}
以上代码并没有实现防抖功能,因为每次执行 proxy,init 都是 true,虽然 setTimeout 中不会再调用 handle 了,但是 line 12 处的代码每次都会执行。
分析:timer 变量只在第一次是 null,连续的第二次调用 proxy 时,setTimeout 中的代码虽然没有执行,但是 timer 已经不是 null 了。
利用这一点,可以通过 timer 是否为 null 来判断是否是第一次执行。
function debounce(handle, wait, immediate) {
// ...
let timer = null
return function proxy(...args) {
const self = this
const init = immediate
const init = immediate && !timer
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
!init ? handle.call(self, ...args) : null
}, wait)
init ? handle.call(self, ...args) : null
}
}
如上修改之后,执行结果如下图:
可以看到,第一次点击按钮,立即就执行了 handle。虽然防抖功能实现了。但是有个 bug:在最后一次点击停止后,会再次执行一次 handle。分析了一下原因,是因为 line 11 setTimeout 中的代码还是用的 init 来判断,而最后一次点击结束,timer!=null,导致 init=false,因此在 setTimeout 中又执行了一次 handle。只需要通过 immediate 判断即可:
function debounce(handle, wait, immediate) {
// ...
return function proxy(...args) {
// ...
timer = setTimeout(() => {
// ...
!init ? handle.call(self, ...args) : null
!immediate ? handle.call(self, ...args) : null
}, wait)
// ...
}
}
这样便实现了执行第一次的防抖函数。
试一试
Count is:
7、完整代码
点击查看
/**
* handle 需要执行的事件监听
* wait 事件触发后多久开始执行
* immediate 控制执行第一次还是最后一次,false:最后一次,true:第一次
*/
function debounce(handle, wait, immediate) {
if (typeof handle !== 'function') {
throw new Error('handle must be a function')
}
if (typeof wait === 'boolean') {
immediate = wait
wait = 300
}
if (typeof wait !== 'number') {
wait = 300
}
if (typeof immediate !== 'boolean') {
immediate = false
}
let timer = null
return function proxy(...args) {
const self = this
const init = immediate && !timer
clearTimeout(timer)
timer = setTimeout(() => {
timer = null
!immediate ? handle.call(self, ...args) : null
}, wait)
init ? handle.call(self, ...args) : null
}
}
节流函数
对于高频操作,我们可以自己来设置频率,让本来会执行很多次的事件触发,按着我们定义的频率减少触发的次数
应用场景
前置场景
页面有一个很高的高度。
body {
height: 5000px;
}
页面滚动时,执行方法。
function scroll() {
console.log('滚动了')
}
window.onscroll = scroll
没有使用节流函数,在页面滚动的过程中 scroll 函数就会高频触发。
实现节流函数
1、思路
如图,我们需要定义变量 prev 记录上一次执行的时间点。
let prev = 0 // 记录上一次执行的时间点
还需要记录当前次执行的时间点,以及计算出时间差。
const now = new Date() // 记录当前次执行的时间点
const interval = wait - (now - prev)
然后根据时间差进行判断。并重新设置 prev,以及开启定时器。
if (interval <= 0) {
handle.call(self, ...args)
prev = new Date()
} else {
// 此时说明这次操作发生在定义的时间范围内,不应该执行handle
// 这个时候需要定义一个定时器,让handle在interval之后再执行
setTimeout(() => {
handle.call(self, ...args)
prev = new Date()
}, interval)
}
2、考虑一种场景。
当发现当前系统中有一个定时器了,就意味着不需要再开启定时器了。修改代码如下:
let timer = null // 用来管理定时器
// ...
if (interval <= 0) {
clearTimeout(timer)
timer = null
handle.call(self, ...args)
prev = new Date()
} else if (!timer) {
timer = setTimeout(() => {
clearTimeout(timer) // 这个操作只是将系统中的定时器清除了,但是timer中的值还在
timer = null // 因此需要timer=null
handle.call(self, ...args)
prev = new Date()
}, interval)
}
这样就实现了节流函数。
试一试
ScrollTop with throttle is:
ScrollTop without throttle is:
3、完整代码
点击查看
function throttle(handle, wait) {
if (typeof handle !== 'function') {
throw new Error('handle must be a function')
}
if (typeof wait === 'undefined') {
wait = 400
}
let prev = 0
let timer = null
return function proxy(...args) {
const self = this
const now = new Date()
const interval = wait - (now - prev)
if (interval <= 0) {
clearTimeout(timer)
timer = null
handle.call(self, ...args)
prev = new Date()
} else if (!timer) {
timer = setTimeout(() => {
clearTimeout(timer)
timer = null
handle.call(self, ...args)
prev = new Date()
}, interval)
}
}
}