防抖和节流
1.认识防抖debounce函数
理解:
- 当事件触发时,响应的函数并不会立即触发,而是会等待一定的时间
- 当事件密集触发时,函数的触发会被频繁的推迟
- 只有等待了一段时间没有事件触发,才会真正的执行响应函数
简单来说就是延迟执行,每次都重新计时,最终只执行一次
应用场景:
- 输入框中频繁的输入内容,搜索或者提交信息
- 频繁的点击按钮,触发某个事件
- 监听浏览器滚动事件,完成某些特定操作
- 用户缩放浏览器的resize事件
2.认识节流throttle函数
理解:
- 当事件触发时,会执行这个事件的响应函数
- 如果这个事件被频繁触发,那么节流函数会按照一定的频率来执行函数
- 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的
简单来说就是,减少了执行次数,在指定的时间间隔就会执行一次
应用场景:(其实跟防抖差不多,这是两种不同的解决方案)
- 用户频繁点击按钮操作
- 鼠标移动事件
- 监听页面的滚动事件
3.第三方库
两个用的比较多的:lodash、underscore
示例:可以直接引入,也可以npm下载
<input type="text">
<button id="cancel">取消</button>
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
<script>
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function(event) {
console.log(`发送了第${++counter}次网络请求`, this, event);
}
// 防抖
inputEl.oninput = _.debounce(inputChange, 1000)
// 节流
inputEl.oninput = _.throttle(inputChange, 1000)
</script>
4.手写防抖debounce函数
思路:
- 防抖基本功能实现:可以实现防抖效果
- 优化一:优化参数和this指向
- 优化二:优化立即执行效果(第一次立即执行)
- 优化三:优化取消操作(增加取消功能)
- 优化四:优化返回值
开始实现:
- 基本功能实现
function debounce(fn, delay) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null
// 2.真正执行的函数
const _debounce = function() {
// 取消上一次的定时器
if (timer) clearTimeout(timer)
// 延迟执行
timer = setTimeout(() => {
// 外部传入的真正要执行的函数
fn()
}, delay)
}
return _debounce
}
- 优化参数和this指向:用apply调用,因为_debounce ,实际上执行的是inputEl.oninput,隐式绑定inputEl,this向上层找到
function debounce(fn, delay) {
let timer = null
const _debounce = function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
// 用apply去调用
fn.apply(this, args)
}, delay)
}
return _debounce
}
- 第一次立即执行:记录有没有被立即执行过,否则每次都会立即执行
function debounce(fn, delay, immediate = false) {
let timer = null
// 用于记录有没有被立即执行
let isInvoke = false
const _debounce = function(...args) {
if (timer) clearTimeout(timer)
// 判断是否需要立即执行,如果传入的immediate为true,并且函数没有被立即执行过,那么需要立即执行
if (immediate && !isInvoke) {
fn.apply(this, args)
isInvoke = true
} else {
// 延迟执行
timer = setTimeout(() => {
fn.apply(this, args)
// 做一些清除工作
isInvoke = false
timer = null
}, delay)
}
}
return _debounce
}
- 取消功能:清除定时器
function debounce(fn, delay, immediate = false) {
let timer = null
let isInvoke = false
const _debounce = function(...args) {
if (timer) clearTimeout(timer)
if (immediate && !isInvoke) {
fn.apply(this, args)
isInvoke = true
} else {
timer = setTimeout(() => {
fn.apply(this, args)
isInvoke = false
timer = null
}, delay)
}
}
// 封装取消功能
_debounce.cancel = function() {
// 把定时器清除就行
if (timer) clearTimeout(timer)
timer = null
isInvoke = false
}
return _debounce
}
- 函数返回值:两种方式:传入回调函数,或者用promise
function debounce(fn, delay, immediate = false, resultCallback) {
let timer = null
let isInvoke = false
const _debounce = function(...args) {
return new Promise((resolve, reject) => {
if (timer) clearTimeout(timer)
if (immediate && !isInvoke) {
// 拿到函数的返回结果
const result = fn.apply(this, args)
// 通过回调函数返回出去
if (resultCallback) resultCallback(result)
// 或者通过Promise的resolve返回出去
resolve(result)
isInvoke = true
} else {
// 延迟执行
timer = setTimeout(() => {
// 外部传入的真正要执行的函数
const result = fn.apply(this, args)
if (resultCallback) resultCallback(result)
resolve(result)
isInvoke = false
timer = null
}, delay)
}
})
}
_debounce.cancel = function() {
if (timer) clearTimeout(timer)
timer = null
isInvoke = false
}
return _debounce
}
5.手写节流throttle函数
思路:
- 节流函数的基本实现:可以实现节流效果
- 优化一:节流最后一次也可以执行
- 优化二:优化添加取消功能
- 优化三:优化返回值问题
开始实现:
- 基本实现
function throttle(fn, interval) {
// 1.记录上一次的开始时间
let lastTime = 0
// 2.事件触发时, 真正执行的函数
const _throttle = function() {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 2.3.真正触发函数
fn()
// 2.4.保留上次触发的时间
lastTime = nowTime
}
}
return _throttle
}
- leading的实现,也就是控制需不需要立即执行(默认执行)
首先要知道为什么会立即执行,由于我们设置的lastTime是0,而一开始nowTime是一个很大很大的数,那么remainTime计算出来就是负数,所以会执行
function throttle(fn, interval, options = { leading: true}) {
const { leading } = options
let lastTime = 0
const _throttle = function() {
const nowTime = new Date().getTime()
// 如果lastTime为0,并且有传入leading为false
if (!lastTime && !leading) lastTime = nowTime
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
fn()
lastTime = nowTime
}
}
return _throttle
}
- trailing实现:最后一次要不要执行(默认不执行)
注意跟leading冲突的问题,不要让函数执行两次
例如:如果lastTime直接初始化为0,10s的时候定时器到点执行了一次,10.1s的时候首次执行又执行了一次
function throttle(fn, interval, options = { leading: true, trailing: false }) {
const { leading, trailing } = options
let lastTime = 0
let timer = null
const _throttle = function () {
const nowTime = new Date().getTime()
if (!lastTime && !leading) lastTime = nowTime
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 因为在前面执行的时候也会加上定时器
// 但是如果我们这个fn是时间到了正常执行了的话,是不需要定时器的,所以在执行之前要把定时器清除掉
// 后面才能加上我们想要的定时器
if (timer) {
clearTimeout(timer)
timer = null
}
fn()
lastTime = nowTime
// 直接return,不往下加定时器
return
}
if (trailing && !timer) {
timer = setTimeout(() => {
fn()
// 针对我们下一次的操作(停了很久重新输入)
// 清除定时器, lastTime置0
timer = null
// 解决leading与trailing冲突
lastTime = !leading ? 0 : new Date().getTime()
}, remainTime);
}
}
return _throttle
}
- this和参数问题,返回值问题优化
function throttle(fn, interval, options = { leading: true, trailing: false }) {
const { leading, trailing } = options
let lastTime = 0
let timer = null
const _throttle = function (...args) {
// 使用promise的resolve返回结果
return new Promise((resolve, reject) => {
const nowTime = new Date().getTime()
if (!lastTime && !leading) lastTime = nowTime
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
if (timer) {
clearTimeout(timer)
timer = null
}
// apply 调用
fn.apply(this, args)
lastTime = nowTime
return
}
if (trailing && !timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
lastTime = !leading ? 0 : new Date().getTime()
}, remainTime)
}
})
}
return _throttle
}
- 取消功能
_throttle.cancel = function() {
if(timer) clearTimeout(timer)
timer = null
lastTime = 0
}