目录
- 认识防抖与节流
- 防抖
- 节流
- 手写防抖函数
- 绑定this与参数
- 取消功能
- 立即执行
- 获取返回值
- 最终版
- 手写节流函数
认识防抖与节流
在JavaScript
中,大量操作
都会触发事件
,这些事件
又会被添加到事件队列
中进行排队处理
某些事件如果频繁触发的话会对浏览器的性能造成损耗
,我们就可以使用防抖
或者节流
操作来限制
事件的执行频率
防抖
防抖
即当一个事件被触发时不会立即执行
,而是会等待一段时间
后执行,在等待期间如果此事件被重复触发
,则等待时间也会重新计算
,只有在等待期间内没有事件触发
,等待时间结束后才会触发事件的回调函数
简单地说,王者里的回城就是一种防抖
节流
节流
即当事件触发时会执行事件的回调函数
,之后这个事件将会等待一段时间
,在这段时间内触发的事件都将不会执行
,直到等待时间结束
后如果依旧此事件触发才会执行此事件
的回调函数,之后继续等待
简单地说,王者里的技能就是一种节流
如下图所示
手写防抖函数
const debounce = (fn, delay) => {
let timer = null
return () => {
timer && clearTimeout(timer)
timer = setTimeout(fn, delay)
}
}
const resize = debounce(() => {
console.log('resize')
}, 1000)
window.addEventListener('resize', resize)
结果如下
但这个函数十分简陋,没有绑定this
,也没有参数传递
,接下来我们一步一步来实现这些功能
绑定this与参数
绑定this
有两种方式,一种是通过在参数列表中
传入this指向
,然后通过显式绑定
到对应的对象
上,还有一种是将返回的箭头函数
改成普通函数
- 第一种方式
const debounce = (fn, {delay,that}={}) => {
let timer = null
return () => {
timer && clearTimeout(timer)
timer = setTimeout(fn.apply(that), delay)
}
}
const resize = debounce(() => {
console.log(this)
}, {
delay:1000,
that:window
})
window.addEventListener('resize', resize)
这段代码粗看似乎没什么问题,但实测发现防抖
函数无效,因为apply会立即调用函数
,解决方法是将apply封装到一个函数中
const debounce = (fn, { delay, that } = {}) => {
let timer = null
return () => {
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(that)
}, delay)
}
}
const resize = debounce(() => {
console.log(this)
}, {
delay: 1000,
that: window
})
window.addEventListener('resize', resize)
当我们需要传递的参数
过多时可以通过参数解构与默认值
的方式获取
2. 第二种方式
const debounce = (fn, delay) => {
let timer = null
return function () {
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this)
}, delay)
}
}
const resize = debounce(() => {
console.log(this)
}, 1000)
window.addEventListener('resize', resize)
最后结果都是一样的
参数的绑定
十分简单,这里就不再赘述了
const debounce = (fn, delay) => {
let timer = null
return function (...args) {
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
const resize = debounce((event) => {
console.log(this, event)
}, 1000)
window.addEventListener('resize', resize)
取消功能
有时候事件触发了但我们之后又不想函数执行,可以增加一个取消
功能,我们可以在返回的函数
上直接添加一个属性cancel
const debounce = (fn, delay) => {
let timer = null
const _debounce = function (...args) {
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
_debounce.cancel = function () {
timer && clearTimeout(timer)
}
return _debounce
}
const resize = debounce((event) => {
console.log(this, event)
}, 1000)
window.addEventListener('resize', resize)
立即执行
立即执行
即我们需要在事件触发时的第一次
就执行函数
const debounce = (fn, { delay = 1000, immediate = false } = {}) => {
let timer = null
let isInvoke = false
const _debounce = function (...args) {
if (immediate && !isInvoke) {
fn.apply(this, args)
isInvoke = true
return
}
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
_debounce.cancel = function () {
timer && clearTimeout(timer)
}
return _debounce
}
const resize = debounce((event) => {
console.log(this, event)
}, {
delay: 1000,
immediate: true
})
window.addEventListener('resize', resize)
获取返回值
有时候我们在手动调用防抖函数的时候需要得到函数的返回值
就可以这么写,第一种方案是通过回调函数
,第二种则是返回一个Promise
- 传入一个
回调函数
来获取返回值
const debounce = (fn, { delay = 1000, immediate = false, callback } = {}) => {
let timer = null
let isInvoke = false
const _debounce = function (...args) {
if (immediate && !isInvoke) {
fn.apply(this, args)
isInvoke = true
return
}
timer && clearTimeout(timer)
timer = setTimeout(() => {
let res = fn.apply(this, args)
callback && callback(res)
}, delay)
}
_debounce.cancel = function () {
timer && clearTimeout(timer)
}
return _debounce
}
const resize = debounce((event) => {
console.log(this)
return "resize return"
}, {
delay: 1000,
immediate: false,
callback: (res) => {
console.log(res)
}
})
resize()
- 返回一个
Promise
得到返回值
const debounce = (fn, { delay = 1000, immediate = false } = {}) => {
let timer = null
let isInvoke = false
const _debounce = function (...args) {
return new Promise((resolve, reject) => {
if (immediate && !isInvoke) {
fn.apply(this, args)
isInvoke = true
return
}
timer && clearTimeout(timer)
timer = setTimeout(() => {
let res = fn.apply(this, args)
resolve(res)
}, delay)
})
}
_debounce.cancel = function () {
timer && clearTimeout(timer)
}
return _debounce
}
const resize = debounce((event) => {
console.log(this)
return "resize return"
}, {
delay: 1000,
immediate: false
})
resize().then((res) => {
console.log(res)
})
结果都是一样的
最终版
最后我们将以上代码优化一下就得到了最终版本的防抖函数
const debounce = (fn, { delay = 1000, immediate = false } = {}) => {
let timer = null
let isInvoke = false
const _debounce = function (...args) {
return new Promise((resolve, reject) => {
try {
if (immediate && !isInvoke) {
let res = fn.apply(this, args)
isInvoke = true
resolve(res)
return
}
timer && clearTimeout(timer)
timer = setTimeout(() => {
let res = fn.apply(this, args)
timer = null
isInvoke = false
resolve(res)
}, delay)
} catch (error) {
reject(error)
}
})
}
_debounce.cancel = function () {
timer && clearTimeout(timer)
isInvoke = false
timer = null
}
return _debounce
}
手写节流函数
节流函数
在实现上和防抖函数
稍有不同,不通过定时器
而是通过时间戳
以下是一个简略的节流函数实现
const throttle = (fn, { wait = 1000, }) => {
let preTime = 0;
const _throttle = function (...args) {
let nowTime = Date.now();
if (nowTime - preTime > wait) {
fn.apply(this, args);
preTime = nowTime;
}
}
return _throttle
}
const resize = throttle(function () {
console.log("resize")
}, {
wait: 1000,
})
window.addEventListener('resize', resize)
结果如下
至于节流函数
的一些优化:
节流函数立即执行与尾部执行
添加取消功能
获得返回值
与防抖函数的思路大差不差,这里就不再过多赘述,以下是完全版
const throttle = (fn, { wait = 1000, leading = true, trailing = false } = {}) => {
let preTime = 0;
let timer
const _throttle = function (...args) {
return Promise((resolve, reject) => {
try {
let nowTime = Date.now();
if (!leading && preTime == 0) {
preTime = nowTime
}
let interval = wait - (nowTime - preTime)
if (interval <= 0) {
let res = fn.apply(this, args)
resolve(res)
if (timer) clearTimeout(timer)
preTime = nowTime
timer = null
return
}
if (trailing && !timer) {
timer = setTimeout(() => {
let res = fn.apply(this, args)
resolve(res)
preTime = Date.now()
timer = null
}, interval)
}
} catch (error) {
reject(error)
}
})
}
_throttle.cancel = function () {
if (timer) clearTimeout(timer)
preTime = 0
timer = null
}
return _throttle
}