@色少8年前
05/11
19:17
项目优化种发现很多时候需要输入之后验证,因此用得频率比较高的2个函数debounce和throttle。还有以下场景往往由于事件频繁被触发,因而频繁执行DOM操作、资源加载等重行为,导致UI停顿甚至浏览器崩溃。
1. window对象的resize、scroll事件
2. 拖拽时的mousemove事件
3. 射击游戏中的mousedown、keydown事件
4. 文字输入、自动完成的keyup事件
实际上对于window的resize事件,实际需求大多为停止改变大小n毫秒后执行后续处理;而其他事件大多的需求是以一定的频率执行后续处理。针对这两种需求就出现了debounce和throttle两种解决办法。
debounce
debounce的关注点是空闲的间隔时间
// 函数去抖(连续事件触发结束后只触发一次) // sample 1: _.debounce(function(){}, 1000) // 连续事件结束后的 1000ms 后触发 // sample 1: _.debounce(function(){}, 1000, true) // 连续事件触发后立即触发(此时会忽略第二个参数) _.debounce = function(func, wait, immediate) { // immediate默认为false var timeout, args, context, timestamp, result; var later = function() { // 当wait指定的时间间隔期间多次调用_.debounce返回的函数,则会不断更新timestamp的值,导致last < wait && last >= 0一直为true,从而不断启动新的计时器延时执行func var last = _.now() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) { result = func.apply(context, args); if (!timeout) context = args = null; } } }; return function() { context = this; args = arguments; timestamp = _.now(); // 第一次调用该方法时,且immediate为true,则调用func函数 var callNow = immediate && !timeout; // 在wait指定的时间间隔内首次调用该方法,则启动计时器定时调用func函数 if (!timeout) timeout = setTimeout(later, wait); if (callNow) { result = func.apply(context, args); context = args = null; } return result; }; };
Throttle
throttle 的关注点是连续的执行间隔时间
// 函数节流(如果有连续事件响应,则每间隔一定时间段触发) // 每间隔 wait(Number) milliseconds 触发一次 func 方法 // 如果 options 参数传入 {leading: false} // 那么不会马上触发(等待 wait milliseconds 后第一次触发 func) // 如果 options 参数传入 {trailing: false} // 那么最后一次回调不会被触发 // **Notice: options 不能同时设置 leading 和 trailing 为 false** // 示例: // var throttled = _.throttle(updatePosition, 100); // $(window).scroll(throttled); // 调用方式(注意看 A 和 B console.log 打印的位置): // _.throttle(function, wait, [options]) // sample 1: _.throttle(function(){}, 1000) // print: A, B, B, B ... // sample 2: _.throttle(function(){}, 1000, {leading: false}) // print: B, B, B, B ... // sample 3: _.throttle(function(){}, 1000, {trailing: false}) // print: A, A, A, A ... // ----------------------------------------- // _.throttle = function(func, wait, options) { /* options的默认值 * 表示首次调用返回值方法时,会马上调用func;否则仅会记录当前时刻,当第二次调用的时间间隔超过wait时,才调用func。 * options.leading = true; * 表示当调用方法时,未到达wait指定的时间间隔,则启动计时器延迟调用func函数,若后续在既未达到wait指定的时间间隔和func函数又未被调用的情况下调用返回值方法,则被调用请求将被丢弃。 * options.trailing = true; * 注意:当options.trailing = false时,效果与上面的简单实现效果相同 */ var context, args, result; var timeout = null; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : _.now(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { var now = _.now(); if (!previous && options.leading === false) previous = now; // 计算剩余时间 var remaining = wait - (now - previous); context = this; args = arguments; // 当到达wait指定的时间间隔,则调用func函数 // 精彩之处:按理来说remaining <= 0已经足够证明已经到达wait的时间间隔,但这里还考虑到假如客户端修改了系统时间则马上执行func函数。 if (remaining <= 0 || remaining > wait) { // 由于setTimeout存在最小时间精度问题,因此会存在到达wait的时间间隔,但之前设置的setTimeout操作还没被执行,因此为保险起见,这里先清理setTimeout操作 if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // options.trailing=true时,延时执行func函数 timeout = setTimeout(later, remaining); } return result; }; };
throttle 应用场景
- DOM 元素的拖拽功能实现(mousemove)
- 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
- 计算鼠标移动的距离(mousemove)
- Canvas 模拟画板功能(mousemove)
- 搜索联想(keyup)
- 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次
debounce 应用场景
函数去抖有哪些应用场景?哪些时候对于连续的事件响应我们只需要执行一次回调?
- 每次 resize/scroll 触发统计事件
- 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)