在做前端可视化的某些内容轮播时,使用点击事件会用到节流(立即执行),防止动画执行时被多次点击事件打断导致某些奇怪错误。总结记录下防抖(debounce)
和节流(throttle)
及他们的不同效果的用法。
防抖和节流定义的区别 首先分析一下防抖和节流的区别:
防抖(debounce)
同样也是稀释高频事件的执行频率,但是防抖是设置了一个延迟时间
来执行回调函数,如果在延迟期间内再次触发事件,会重新计算延迟时间,可分为立即执行和非立即执行两种版本。
节流(throttle)
比较容易理解,如果想稀释高频事件的回调函数的执行频率,就设置一个周期时间
,在一个周期内回调函数只能执行一次,可分为有头有尾、有头无尾、无头有尾三种。
注意 1:箭头函数没有自身的 arguments
和 this
,需要去上下文中寻找。所以本文中 setTimeout
的回调均使用的箭头函数,arguments
和 this
在它的上层作用域中。
注意 2:debounce
和 throttle
函数都是返回了一个 debounced
或 throttled
作为真正的回调函数。debounced
和 throttled
都利用了闭包在 debounce
和 throttle
存放一些“通用”变量。
防抖(debounce) 先放出防抖的代码,debounce
接受三个参数,分别是回调函数 func
,延迟时间 wait
和是否立即执行 immediate
,又给防抖函数的 return
添加了一个取消防抖 cancel
的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const debounce = function (func, wait, immediate = false ) { let timeoutID, result; let debounced = function ( ) { window .clearTimeout(timeoutID); if (immediate === true ) { if (!timeoutID) result = func.apply(this , arguments ); timeoutID = window .setTimeout(() => { timeoutID = null ; }, wait); } else { timeoutID = window .setTimeout(() => { func.apply(this , arguments ); }, wait); } return result; }; debounced.cancel = function ( ) { window .clearTimeout(timeoutID); timeoutID = null ; }; return debounced; };
逐步分析防抖函数的逻辑,按 immediate
参数可以分为立即执行
和非立即执行
两种:
非立即执行 当默认情况,也就是 immediate
为 false
的情况下,假设执行回调函数为 debounce(foo,10000,false)
,用一个类似伪代码形式模拟步骤,来分析执行逻辑。
由于非立即执行时 func.apply(this, arguments)
返回结果是异步的,所以只能返回 undefined
。
事件第一次触发 1 2 3 4 5 6 7 8 window .clearTimeout(undefined );randomID1 = window .setTimeout(() => { foo.apply(this , arguments ); }, 10000 ); return undefined ;
事件第一次触发后 10s 内的第二次触发 1 2 3 4 5 6 7 8 9 10 11 12 window .clearTimeout(randomID1);randomID2 = window .setTimeout(() => { foo.apply(this , arguments ); }, 10000 ); return undefined ;
事件第二次触发后 10s 外的第三次触发 1 2 3 4 5 6 7 8 9 window .clearTimeout(randomID2);randomID3 = window .setTimeout(() => { foo.apply(this , arguments ); }, 10000 ); return undefined ;
立即执行 当 immediate
为 false
的情况下,假设执行回调函数为 debounce(foo,10000,true)
,继续使用伪代码形式模拟步骤。
立即执行的防抖函数中 func.apply(this, arguments)
是同步执行的,可以拿到 result
。
事件第一次触发 1 2 3 4 5 6 7 8 9 window .clearTimeout(undefined );executedResult1 = foo.apply(this , arguments ); randomID1 = window .setTimeout(() => { randomID1 = null ; }, 10000 );
事件第一次触发后 10s 内的第二次触发 1 2 3 4 5 6 7 8 9 10 11 12 window .clearTimeout(randomID1);randomID2 = window .setTimeout(() => { randomID2 = null ; }, 10000 );
事件第二次触发后 10s 外的第三次触发 1 2 3 4 5 6 7 8 9 10 window .clearTimeout(randomID2);executedResult3 = foo.apply(this , arguments ); randomID3 = window .setTimeout(() => { randomID3 = null ; }, 10000 );
节流(throttle) 节流的代码如下,throttle
接受三个参数,分别是回调函数 func
,延迟时间 wait
和是否开头或结尾执行的 options
,同样给节流函数的 return
添加了一个取消防抖 cancel
的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 const throttle = (func, wait, options = { leading: true , trailing: true } ) => { let timeoutID; let previous = 0 ; let throttled = function ( ) { let now = new Date ().getTime(); if (!previous && options.leading === false ) previous = now; let remaining = wait - (now - previous); if (remaining <= 0 || remaining > wait) { if (timeoutID) { clearTimeout(timeoutID); timeoutID = null ; } previous = now; func.apply(this , arguments ); } else if (!timeoutID && options.trailing !== false ) { timeoutID = window .setTimeout(() => { previous = options.leading === false ? 0 : new Date ().getTime(); timeoutID = null ; func.apply(this , arguments ); }, remaining); } }; throttled.cancel = function ( ) { clearTimeout(timeoutID); previous = 0 ; timeoutID = null ; }; return throttled; };
同样逐步分析节流函数的逻辑,按 options
参数可以分为有头有尾
、有头无尾
、无头有尾
三种(无头无尾
会有 bug)。
有头有尾 假设执行回调函数为 throttle(foo,10000,{ leading: true, trailing: true })
,继续使用伪代码形式模拟步骤。
一步步比较简洁,不再详细解释。
事件第一次触发 1 2 3 4 5 let nowStamp1 = new Date ().getTime();let remainingStamp1 = 10000 - (nowStamp1 - 0 );previousStamp1 = nowStamp1; fool.apply(this , arguments );
事件第一次触发后 10s 内的第二次触发 1 2 3 4 5 6 7 8 let nowStamp2 = new Date ().getTime();let remainingStamp2 = 10000 - (nowStamp2 - nowStamp1);timeoutID2 = window .setTimeout(() => { previousStamp2 = new Date ().getTime(); timeoutID2 = null ; foo.apply(this , arguments ); }, 10000 );
事件第一次触发后 10s 外且第二次触发 10 秒内的第三次触发 1 2 3 4 5 6 7 let nowStamp3 = new Date ().getTime();let remainingStamp3 = 10000 - (nowStamp3 - previousStamp1);clearTimeout(timeoutID2); timeoutID2 = null ; previousStamp3 = nowStamp3; foo.apply(this , arguments );
有头无尾 其实在有头有尾
中已经把有头无尾
解释的差不多,有头无尾
利用的是时间戳
来判断,不涉及到异步 setTimeout
,单纯的有头无尾可以写成:
1 2 3 4 5 6 7 8 9 10 function throttle (func, wait ) { let pre = 0 ; return function ( ) { let now = parseInt (new Date ().getTime()); if (now - pre > wait) { func.apply(this , arguments ); pre = now; } }; }
无头有尾 同样,单纯的无头有尾
利用的是纯异步 setTimeout
,也可以简写成:
1 2 3 4 5 6 7 8 9 10 11 function throttle (func, wait ) { let timeoutID; return function ( ) { if (!timeoutID) { timeoutID = window .setTimeout(() => { timeoutID = null ; func.apply(this , arguments ); }, wait); } }; }
效果对比 借鉴一下Polaris_tl 博客中的展示方式,清晰的展示一下几种不同防抖
和节流
效果的对比:
不同防抖和节流的效果对比
展示代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <meta http-equiv ="X-UA-Compatible" content ="ie=edge" /> <title > 防抖和节流效果对比</title > <style > body { height: 100%; font-family: 'Microsoft YaHei'; font-size : 1.2em ; } div { width: 100%; background-color: lightcyan; overflow: hidden; margin-top: 4px; margin-bottom: 8px; } p { float: left; width: 30px; height: 30px; background-color: pink; margin: 2px; } </style > </head > <body > 防抖(非立即执行): <div id ="d1_1" > </div > 防抖(立即执行): <div id ="d1_2" > </div > 节流(有头有尾): <div id ="d2_1" > </div > 节流(有头无尾): <div id ="d2_2" > </div > 节流(无头有尾): <div id ="d2_3" > </div > </body > <script > /** * @description: 防抖函数 * @param {Function } func 回调函数 * @param {Number } wait 等待时间 ms * @param {Boolean } immediate 是否立即执行 * @return : result func返回的结果 */ const debounce = function (func, wait, immediate = false) { let timeoutID, result; let debounced = function ( ) { window .clearTimeout(timeoutID); if (immediate === true ) { if (!timeoutID) result = func.apply(this , arguments ); timeoutID = window .setTimeout(() => { timeoutID = null ; }, wait); } else { timeoutID = window .setTimeout(() => { func.apply(this , arguments ); }, wait); } return result; }; debounced.cancel = function () { window .clearTimeout(timeoutID); timeoutID = null ; }; return debounced; }; /** * @description: 节流函数 * @param {Function } func 回调函数 * @param {Number } wait 等待时间 ms * @param {Object } options leading代表是否开头执行,tailing代表是否结尾执行 * @return : null */ const throttle = ( func, wait, options = { leading: true , trailing: true } ) => { let timeoutID; let previous = 0 ; let throttled = function ( ) { let now = new Date ().getTime(); if (!previous && options.leading === false ) previous = now; let remaining = wait - (now - previous); if (remaining <= 0 || remaining > wait) { if (timeoutID) { clearTimeout(timeoutID); timeoutID = null ; } previous = now; func.apply(this , arguments ); } else if (!timeoutID && options.trailing !== false ) { timeoutID = window .setTimeout(() => { previous = options.leading === false ? 0 : new Date ().getTime(); timeoutID = null ; func.apply(this , arguments ); }, remaining); } }; throttled.cancel = function () { clearTimeout(timeoutID); previous = 0; timeoutID = null ; }; return throttled; }; const addElement = function (f) { let node = document .createElement('p' ); f.appendChild(node); }; const debounce_fn1 = function () { addElement(d1_1); }; const debounce_fn2 = function () { addElement(d1_2); }; const throttle_fn1 = function () { addElement(d2_1); }; const throttle_fn2 = function () { addElement(d2_2); }; const throttle_fn3 = function () { addElement(d2_3); }; let body = document .getElementsByTagName('body' )[0 ]; body.addEventListener('mousemove' , debounce(debounce_fn1, 500 , false )); body.addEventListener('mousemove' , debounce(debounce_fn2, 500 , true )); body.addEventListener( 'mousemove' , throttle(throttle_fn1, 500 , { leading: true , trailing: true }) ); body.addEventListener( 'mousemove' , throttle(throttle_fn2, 500 , { leading: true , trailing: false }) ); body.addEventListener( 'mousemove' , throttle(throttle_fn3, 500 , { leading: false , trailing: true }) ); </script > </html >