防抖(debounce)
当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。
/**
* @desc 防抖函数
* @param {function} fn 需要进行防抖控制的函数
* @typedef {Object} options 防抖配置参数
* @property {number} delay 防抖频率,单位毫秒ms
* @property {boolean} immediate 是否立即执行
* @returns {function}
*/
function debounce(fn: (...args: any) => any, options: DebounceOptions) {
const { delay = 100, immediate = false } = options || {}
let timer
return function (...args) {
if (timer) {
clearTimeout(timer)
}
if (immediate) {
const callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay)
if (callNow) {
fn.apply(this, args)
}
} else {
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
}
异步防抖(asyncDebounce)
/**
* @desc 异步防抖函数
* @param {function} fn 需要进行防抖控制的函数
* @typedef {Object} options 防抖配置参数
* @property {number} delay 防抖频率,单位毫秒ms
* @property {boolean} immediate 是否立即执行
* @returns {function}
*/
function asyncDebounce(fn: (...args: any) => any, options: DebounceOptions) {
const { delay = 100, immediate = false } = options || {}
let timer
return function (...args) {
return new Promise((resolve) => {
let result
if (timer) {
clearTimeout(timer)
}
if (immediate) {
const callNow = !timer
timer = setTimeout(() => {
timer = null
}, delay)
if (callNow) {
result = fn.apply(this, args)
resolve(result)
}
} else {
timer = setTimeout(() => {
result = fn.apply(this, args)
resolve(result)
}, delay)
}
})
}
}
节流(throttle)
当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
函数节流主要有两种实现方法:时间戳和定时器。
时间戳
/**
* @desc 节流函数
* @param {function} fn 需要进行节流控制的函数
* @typedef {Object} options 节流配置参数
* @property {number} interval 节流频率,单位毫秒ms
* @property {boolean} immediate 开始是否立即执行
* @returns {function}
*/
function throttle(fn: (...args: any) => any, options: ThrottleOptions) {
const { interval = 100, immediate = false } = options || {}
// immediate=true表示开始时就要执行一次fn,此时将开始时间置成0
let startTime = immediate ? 0 : Date.now()
return function (...args) {
const endTime = Date.now()
if (endTime - startTime >= interval) {
fn.apply(this, args)
startTime = endTime
}
}
}
定时器
var throttle = function(func, delay) {
var timer = null;
return function() {
var context = this;
var args = arguments;
if (!timer) {
timer = setTimeout(function() {
func.apply(context, args);
timer = null;
}, delay);
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
时间戳+定时器
var throttle = function(func, delay) {
var timer = null;
var startTime = Date.now();
return function() {
var curTime = Date.now();
var remaining = delay - (curTime - startTime);
var context = this;
var args = arguments;
clearTimeout(timer);
if (remaining <= 0) {
func.apply(context, args);
startTime = Date.now();
} else {
timer = setTimeout(func, remaining);
}
}}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
1. 防抖(debounce)
- 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
- 当事件密集触发时,函数的触发会被频繁的推迟;
- 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
1.1 应用场景
例如: 输入框中频繁的输入内容 频繁的点击按钮,触发某个事件 监听浏览器滚动事件,完成某些特定操作 用户缩放浏览器的resize事件等等
1.2 简单实现
第一个参数为函数 fn, 第二个参数延迟时间 delay
js复制代码function debounce(fn, delay) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null
// 2.真正执行的函数
const _debounce = function() {
// 取消上一次的定时器
if (timer) clearTimeout(timer)
// 延迟执行
timer = setTimeout(() => {
// 外部传入的真正要执行的函数
fn()
}, delay)
}
return _debounce
}
至此, 已经实现了一个简单的防抖函数
1.3 绑定this和参数args
当原始函数有参数和需要用到this时, 可以给_debounc添加args接收参数并在调用函数fn时使用 fn.apply(this, args)
js复制代码function debounce(fn, delay) {
let timer = null
// 原始函数的参数args
const _debounce = function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
// 绑定this
fn.apply(this, args)
}, delay)
}
return _debounce
}
1.4 第一次是否执行
首次触发时是否立即执行 接收一个参数 immediate, 定义一个默认值, 这里为false, 声明一个是否第一次激活的变量 isInvoke = false, 如果
immediate && !isInvoke就执行函数并将 isInvoke = true, 如下代码
js复制代码function debounce(fn, delay, immediate = false) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null
let isInvoke = false // 是否激活了立即执行
// 2.真正执行的函数
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 // 将isInvoke初始化
}, delay)
}
}
return _debounce
}
1.5 增加取消方法
如果触发了事件又不需要了, 比如直接离开此位置, 增加取消功能防止事件触发带来意外不当
js复制代码function debounce(fn, delay, immediate = false) {
// 1.定义一个定时器, 保存上一次的定时器
...
// 2.真正执行的函数
const _debounce = function(...args) {
...
}
// 封装取消功能
_debounce.cancel = function() {
if (timer) clearTimeout(timer)
timer = null
isInvoke = false
}
return _debounce
}
-
使用时便可以直接使用cancel方法
js复制代码function fn() { ... } const debounceAfterFn = debounce(fn, 1000) // 使用取消方法 debounceAfterFn.cancel()
1.6 函数返回值
解决返回值问题, _debounce返回一个Promise, 调用原始函数fn时拿到返回值
const result = fn.apply(this, args), 再调用resolve(result), 使用时通过.then获取返回值
js复制代码// debounce.js
function debounce(fn, delay, immediate = false) {
// 1.定义一个定时器, 保存上一次的定时器
let timer = null
let isInvoke = false
// 2.真正执行的函数
const _debounce = function(...args) {
return new Promise((resolve, reject) => {
// 取消上一次的定时器
if (timer) clearTimeout(timer)
// 判断是否需要立即执行
if (immediate && !isInvoke) {
const result = fn.apply(this, args)
resolve(result)
isInvoke = true
} else {
// 延迟执行
timer = setTimeout(() => {
// 外部传入的真正要执行的函数, 拿到函数返回值并调用resolve
const result = fn.apply(this, args)
resolve(result)
isInvoke = false
timer = null
}, delay)
}
})
}
// 封装取消功能
_debounce.cancel = function() {
if (timer) clearTimeout(timer)
timer = null
isInvoke = false
}
return _debounce
}
防抖的使用
在html中引入写好的防抖函数的文件debounce.js
html复制代码<input type="text">
<button id="cancel">取消</button>
<script src="debounce.js"></script>
<script>
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function(event) {
console.log(`触发第${++counter}次`, this, event)
// 返回值
return "aaaaaaaaaaaa"
}
// 防抖处理 将原本函数放入 debounce 作为参数, 之后直接使用 debounceChange 即可
const debounceChange = debounce(inputChange, 3000, false)
//文本框事件触发
inputEl.oninput = (...args) => {
// 这种调用方法需要重新绑定this, 所写的debounce函数是没有问题的, 在实际开发中使用相对这里会简单一点
debounceChange.apply(inputEl, args).then(res => {
console.log("Promise的返回值结果:", res)
})
}
// 取消功能
const cancelBtn = document.querySelector("#cancel")
cancelBtn.onclick = function() {
debounceChange.cancel()
}
</script>
2. 节流(throttle)
- 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
- 不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的;
2.1 应用场景
例如: 鼠标移动事件 王者荣耀攻击键, 点击再快也是以一定攻速(频率)进行攻击等等
2.2 简单实现
时间间隔: interval; 当前时间: nowTime; 上次执行时间: lastTime
- 主要算出每次点击后剩余的时间
- 时间差 = 当前时间 - 上次执行时间
- 剩余时间 = 时间间隔 - 时间差
const remainTime = interval - (nowTime - lastTime)判断当剩余时间 <= 0 时执行函数
每次执行完之后将 lastTime = nowTime
js复制代码function throttle(fn, interval, options) {
// 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
}
2.3 首次不触发leading和最后结束触发trailing
2.3.1 leading
初始值lastTime = 0, 所以第一次的nowTime - lastTime也会很大, interval减去很大的正数, 就会成为负数 < 0 所以默认第一次是执行的
如果 interval 不是特别大, 第一次就是执行的, 除非你设置的 interval 大于当前时间. 也就是从计算机元年(1970)到如今, 五十多年的频率, 这个节流可不简单 :)
如果想要第一次不执行, 拿到remainTime之前将 lastTime = nowTime 即可
js复制代码function throttle(fn, interval, options = { leading: true, trailing: false }) {
// 1.记录上一次的开始时间
const { leading, trailing } = options
let lastTime = 0
// 2.事件触发时, 真正执行的函数
const _throttle = function() {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
if (!lastTime && !leading) lastTime = nowTime
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 2.3.真正触发函数
fn()
// 2.4.保留上次触发的时间
lastTime = nowTime
}
}
return _throttle
}
2.3.2 trailing
最后是否执行, 需要使用到定时器 setTimeout
-
首先定义一个定时器 timer = null
-
如果
remainTime <= 0判断 timer 不为空是就清除定时器并置空, 执行完函数后直接 return -
否则判断 trailing 为 true 且 timer 为 null 时, 设置一个定时器, 延迟时间就是剩余时间 remainTime
-
通过 timer 执行函数后关于 lastTime 最后应该设置为什么, 取决于 leading 的值
- 当 leading 为 true 时, 相当于2.2简单实现, 将lastTime赋值当前时间, 这里重新获取当前时间
lastTime = new Date().getTime() - 当 leading 为 false 时, 直接将 lastTime 初始化为0, 相当于2.3.1 leading, 为0时下次执行将会进入
if (!lastTime && !leading) lastTime = nowTime - 因此
lastTime = !leading ? 0 : new Date().getTime()
js复制代码function throttle(fn, interval, options = { leading: true, trailing: false }) { // 1.记录上一次的开始时间 const { leading, trailing } = options let lastTime = 0 let timer = null
// 2.事件触发时, 真正执行的函数 const _throttle = function() {
// 2.1.获取当前事件触发时的时间 const nowTime = new Date().getTime() if (!lastTime && !leading) lastTime = nowTime // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数 const remainTime = interval - (nowTime - lastTime) if (remainTime <= 0) { if (timer) { clearTimeout(timer) timer = null } // 2.3.真正触发函数 fn() // 2.4.保留上次触发的时间 lastTime = nowTime return } if (trailing && !timer) { timer = setTimeout(() => { timer = null lastTime = !leading ? 0 : new Date().getTime() fn() }, remainTime) }}
return _throttle }
- 当 leading 为 true 时, 相当于2.2简单实现, 将lastTime赋值当前时间, 这里重新获取当前时间
2.4 绑定原本函数的this和参数args && 增加取消方法和函数返回值
与防抖实现相同, 使用apply绑定this和传参数, 返回Promise来解决返回值问题 最终代码如下:
js复制代码function throttle(fn, interval, options = { leading: true, trailing: false }) {
// 1.记录上一次的开始时间
const { leading, trailing, resultCallback } = options
let lastTime = 0
let timer = null
// 2.事件触发时, 真正执行的函数
const _throttle = function(...args) {
return new Promise((resolve, reject) => {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
if (!lastTime && !leading) lastTime = nowTime
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.3.真正触发函数
const result = fn.apply(this, args)
resolve(result)
// 2.4.保留上次触发的时间
lastTime = nowTime
return
}
if (trailing && !timer) {
timer = setTimeout(() => {
timer = null
lastTime = !leading ? 0: new Date().getTime()
const result = fn.apply(this, args)
resolve(result)
}, remainTime)
}
})
}
_throttle.cancel = function() {
if(timer) clearTimeout(timer)
timer = null
lastTime = 0
}
return _throttle
}
也可使使用回调函数callback来处理返回值
节流的使用
html复制代码<input type="text">
<button id="cancel">取消</button>
<script src="throttle.js"></script>
<script>
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function(event) {
console.log(`触发第${++counter}次`, this, event)
return 11111111111
}
// 节流处理
const _throttle = throttle(inputChange, 3000, {
leading: false,
trailing: true,
})
inputEl.oninput = (...args) => {
_throttle.apply(inputEl, args).then(res => {
console.log("Promise的返回值结果:", res)
})
}
// 取消功能
const cancelBtn = document.querySelector("#cancel")
cancelBtn.onclick = function() {
_throttle.cancel()
}
</script>