# 理解节流与防抖函数
节流(throttle)函数,可以降低触发频率,每隔一段时间才去执行一次,常用于 resize, scroll 等触发非常频繁但同时需要及时响应的场景。
防抖(debounce)函数,可以防止用户频繁触发某一操作,当用户不再频繁触发时(比如间隔 300ms),才去执行那个操作,常用于input``onchange
事件,表单提交submit
等。
# 节流函数
# 无定时器版本
/**
* 节流函数,降低触发频率,比如表单提交以及resize,scroll事件等
* @param {Function} fn 回调函数
* @param {number} interval 触发间隔
*/
const throttle = (fn, interval) => {
let last = 0
return (...args) => {
let context = this
let now = Date.now()
// 节流函数在第一次执行的时候一定会触发一次
if (now - last >= interval) {
last = now
fn.apply(context, args)
}
}
}
使用
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)
throttle
是一个高阶函数,传入的参数包含函数,且返回的也是一个函数。所以也自然形成了闭包。
上例中,使用了better_scroll
接收了throttle
返回的函数,在better_scroll
函数内部持续引用这last
的值,导致throttle
内部的值last
一直得不到销毁,所以形成了闭包。
疑问一:既然没有使用定时器,那还有保存this
的必要吗?
个人觉得没必要,因为this
在执行时上下文环境没有发生变化,即由代码let context = this;
到fn.apply(context, args);
上下文环境没有发生变化,所以是没有必要的。
const throttle = (fn, interval) => {
let last = 0
return (...args) => {
let now = Date.now()
// 第一次一定会触发
if (now - last >= interval) {
last = now
fn.apply(this, args)
}
}
}
疑问二: 有必要使用apply
来保存this
并传参吗,下面的代码和上面的代码有区别吗
const throttle = (fn, interval) => {
let last = 0
return (...args) => {
let now = Date.now()
// 第一次一定会触发
if (now - last >= interval) {
last = now
fn(...args)
}
}
}
可以去绑定 this
,因为返回的函数有可能会使用 bind
改变 this
,执行的函数可以实现透传 this
疑问三:args 参数有必要吗?
看一下传 args 效果
const scrollMethod = (...args) => {
console.log('触发滚动事件')
console.log('参数是:', ...args)
}
const better_scroll = throttle(scrollMethod, 1000)
// document.addEventListener('scroll', better_scroll(5, 6, 7)) // 这样传的是函数的执行,只会执行一次
// 传递的是已经执行的函数
const better_scroll_params = better_scroll(5, 6, 7)
document.addEventListener('scroll', better_scroll)
结果只在页面第一次加载的时候执行了该函数,多次滚动并没有触发better_scroll_params
该函数,为什么呢?因为better_scroll_params
函数是已经执行好了的函数,并不是函数的定义,所以也就只在第一次执行了一次。
从上面来看,args 参数确实是没必要的。但是有了 args 易于扩展
# 有定时器版本
定时器版本和无定时器版本主要区别:
- 定时器版本不会立即执行
- 定时器版本会有产生宏任务 setTimoutout
/**
* 定时器版本节流函数
* @param {Function} func
* @param {number} wait
*/
const throttle = (func, wait) => {
let timerId
return function foo() {
let context = this
let args = arguments
// 每隔一段时间触发一次,不需要清除定时器
if (!timerId) {
timerId = setTimeout(function() {
timerId = null
func.apply(context, args)
}, wait)
}
}
}
以上面这个定时器版本,来看待非定时器版本的节流函数存在的几个问题。
- 保存 this 有必要吗?
可以保存,因为在setTimeout
的第一个函数参数中,this
是window
,所以需要把this
执行改为foo
函数的this
。但如果使用了箭头函数,就不用保存,但是个人觉得这里的环境并没有什么实际作用(因为我们不需要重新给 foo 绑定 this)
const throttle = (func, wait) => {
let timerId
return function foo(...args) {
if (!timerId) {
timerId = setTimeout(() => {
timerId = null
func.apply(this, args)
}, wait)
}
}
}
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)
- 有必要使用 apply 吗?主要是为了传递,this,应该使用
- 有必要使用 args 吗?可以不需要
# 补充在 react 中遇到的问题
- 假如为
window
使用addEventListtener
绑定了scroll
事件,那么在组件卸载时使用removeEventlistener
移除事件时,需要保证两个事件函数是同一个引用 - 在
setTimeout
中setState
是同步的,正常情况下setState
是异步的
# 防抖函数
/**
* 防抖函数,频繁操作只会执行最后一次
* @param {Function} func 回调函数
* @param {number} delay 延迟
*/
export const debounce = (func, delay) => {
let timer
return function foo(...args) {
// debugger;
// console.log(this, 98989898);
// let context = this
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
// 普通函数需要执行this func()直接执行this 是window
// 箭头函数 this是静态语法 即与setTimeout所处环境的this一致,即与函数debounceReturnCb环境一致
// apply第二个是数组
// func.apply(context, args)
func.apply(this, args)
clearTimeout(timer)
}, delay)
}
}