防抖函数
一、实现原理
- 在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
二、适用场景
-
按钮提交场景:防止多次提交按钮,只执行最后提交的一次
-
搜索框联想场景:防止联想发送请求,只发送最后一次输入
三、代码实现
- 基础代码,如下所示:
<button onclick="func()">按钮</button>
简易版 (非立即执行版) 实现
- 封装函数,代码如下:
/** * @param {Function} func 需要防抖处理的函数 * @param {Number} wait 时间间隔 * @return {Function} 将 debounce 处理结果当作函数返回 */ function debounce(func, wait) { // 通过闭包缓存一个定时器 id let timeout; // 将 debounce 处理结果当作函数返回 // 触发事件回调时执行这个返回函数 return function () { const context = this const args = arguments // 如果已经设定过定时器就清空上一次的定时器 if (timeout) clearTimeout(timeout) // 开始设定一个新的定时器,定时器结束后执行回调函数 func timeout = setTimeout(function () { func.apply(context, args) }, wait) } }
- 测试示例,代码如下:
// 执行 debounce 函数返回新函数 const func = debounce(() => { console.log("func 防抖函数执行了" + Date.now()) }, 1000) // 停止滑动 1 秒后执行函数 document.addEventListener('scroll', func)
立即执行版实现
- 有时希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
/** * @param {Boolean} immediate 表示第一次是否立即执行 */ function debounce(func, wait, immediate) { let timeout; return function () { const context = this; const args = arguments; if (timeout) clearTimeout(timeout); // ------ 新增部分 start ------ // immediate 为 true 表示第一次触发后执行 if (immediate) { const callNow = !timeout; timeout = setTimeout(function () { timeout = null; }, wait) // timeout 为空表示首次触发 if (callNow) func.apply(context, args) // ------ 新增部分 end ------ } else { timeout = setTimeout(function () { func.apply(context, args) }, wait); } } }
- 测试示例,代码如下:
// 执行 debounce 函数返回新函数 const func = debounce(() => { console.log("func 防抖函数执行了" + Date.now()) }, 1000, true) // 第一次触发 scroll 执行一次 func,后续只有在停止滑动 1 秒后才执行函数 func document.addEventListener('scroll', func)
返回值版实现
-
func 函数可能会有返回值,所以需要返回函数结果,但是当
immediate
为false
的时候,因为使用了setTimeout
,我们将func.apply(context, args)
的返回值赋给变量,最后再return
的时候,值将会一直是undefined
,所以只在immediate
为true
的时候返回函数的执行结果。function debounce(func, wait, immediate) { let timeout, result; return function () { const context = this; const args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { const callNow = !timeout; timeout = setTimeout(function () { timeout = null; }, wait) // ------ 修改部分 start ------ if (callNow) result = func.apply(context, args) // ------ 修改部分 end ------ } else { timeout = setTimeout(function () { func.apply(context, args) }, wait); } // 如果 immediate 为 false 的话,会先进行 return result,此时 result 为 undefined // 再去 执行 setTimeout() 中的操作 return result; } }
-
测试示例,代码如下:
// 执行 debounce 函数返回新函数 const func = debounce(() => { return 1; }, 1000, true) // 第一次触发 scroll 执行一次 func,后续只有在停止滑动 1 秒后才执行函数 func document.addEventListener('scroll', func)
节流函数
一、实现原理
- 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
二、适用场景
-
拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
-
缩放场景:监控浏览器 resize
三、代码实现
使用时间戳实现
-
使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
/** * @param {Function} func 需要执行的函数 * @param {Number} wait 时间间隔 * @return {Function} 将 throttle 处理结果当作函数返回 */ function throttle(func, wait) { // 上一次执行 fn 的时间 let previous = 0; return function () { // 获取当前时间,转换成时间戳,单位毫秒 let now = +new Date(); const context = this; const args = arguments; // 将当前时间和上一次执行函数的时间进行对比 if (now - previous > wait) { // 大于等待时间就把 previous 设置为当前时间并执行函数 func func.apply(context, args); previous = now; } } }
使用定时器实现
-
当触发事件的时候,我们设置一个定时器,在触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。
function throttle(func, wait) { let timeout; return function () { const context = this; const args = arguments; // 如果定时器为空,则为首次触发 if (!timeout) { // 开启定时器 timeout = setTimeout(function () { // 清空定时器 timeout = null; func.apply(context, args) }, wait) } } }
加强版 throttle
-
结合
throttle
和debounce
代码,新增逻辑在于当前触发时间和上次触发的时间差小于时间间隔时,设立一个新的定时器,相当于把debounce
代码放在了小于时间间隔部分。function throttle(fn, wait) { // previous 是上一次执行 fn 的时间 // timer 是定时器 let previous = 0, timer = null // 将 throttle 处理结果当作函数返回 return function (...args) { // 获取当前时间,转换成时间戳,单位毫秒 let now = +new Date() // ------ 新增部分 start ------ // 判断上次触发的时间和本次触发的时间差是否小于时间间隔 if (now - previous < wait) { // 如果小于,则为本次触发操作设立一个新的定时器 // 定时器时间结束后执行函数 fn if (timer) clearTimeout(timer) timer = setTimeout(() => { previous = now fn.apply(this, args) }, wait) // ------ 新增部分 end ------ } else { // 第一次执行 // 或者时间间隔超出了设定的时间间隔,执行函数 fn previous = now fn.apply(this, args) } } }
-
测试示例,代码如下:
// 执行 throttle 函数返回新函数 const func = throttle(function() { console.log('func 节流执行了') }, 1000) // 第一次触发 scroll 执行一次 func,每隔 1 秒后执行一次函数 func,停止滑动 1 秒后再执行函数 func document.addEventListener('scroll', func)