NodeJS异步的特性有时候会导致程序非常的难看,回调一层套着一层,这个时候就要用流程控制模块来控制究竟是同步还是异步了。 Nimble是一个轻量、可移植的函数式流程控制模块。经过最小化和压缩后只有837字节,可以运行在Node.js中,也可以用在各种浏览器中。它整合了underscore和async一些最实用的功能,并且API更简单。 nimble有两个流程控制函数,_.parallel和_.series。顾名思义,我们要用的是第二个,可以让一组函数串行执行的_.series。下面这个命令是用来安装Nimble的: npm install nimble 如果用.series调度执行上面那个解方程的函数,代码应该是这样的: ... var…
Javascript: JS函数节流与防抖 throttle,debounce
throttle 与 debounce 都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。
概念
throttle:连续的时间间隔(每隔一定时间间隔执行callback)。
debounce:空闲的时间间隔(callback执行完,过一定空闲时间间隔再执行callback)。
电梯超时
想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。假设电梯有两种运行策略 throttle 和 debounce ,超时设定为15秒,不考虑容量限制。
throttle 策略的电梯。保证如果电梯第一个人进来后,15秒后准时运送一次,不等待。如果没有人,则待机。
debounce 策略的电梯。如果电梯里有人进来,等待15秒。如果又人进来,15秒等待重新计时,直到15秒超时,开始运送。
<div id="switcher"> <div id="actions"> <button id="use-lodash" data-lodash=false>Using <strong>underscore.js</strong> | <strike>lodash.js</strike></button> <button id="every100">1 event every 100 ms</button> <button id="every300" class="every">2 events every 300 ms</button> </div> </div> <div id="sidebar"> <div id="sidebar-free">Move your mouse here</div> </div> <div id="content"> <h2>Mousemove Events:</h2><div id="allEvents"></div> <h2>Debounce Inmediate: _.debounce(fn, 200, true);</h2><div id="debounce_true"></div> <h2>Debounce Inmediate: $.debounce(200, true, fn);</h2> <div id="debouncejtrue"></div> <h2>Debounce: _.debounce(fn, 200);</h2> <div id="debounce_false"></div> <h2>Debounce: $.debounce(200, false, fn);</h2> <div id="debouncejfalse"></div> <h2>Throttle with trailing: _.throttle(fn, 200);</h2> <div id="throttle_true"></div> <h2>Throttle with trailing: $.throttle(200, false, fn);</h2> <div id="throttlejfalse"></div> <h2>Throttle: $.throttle(200, true, fn);</h2> <div id="throttlejtrue"></div> </div> <a href="https://github.com/dcorb/debounce-throttle"> <img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_white_ffffff.png" alt="Fork me on GitHub"> </a> <style> body { background: #444444; color: white; font: 15px/1.51 Helvetica, sans-serif; overflow:hidden; } #switcher { text-align:center; } button { cursor: pointer; color: black; border: 1px solid #999; background: #DADADA; padding: 2px 8px; -moz-border-radius: 4px; -webkit-border-radius: 4px; border-radius: 4px; background-image: -moz-linear-gradient(top,#EBEBEB,#B8B8B8); background-image: -o-linear-gradient(top,#EBEBEB,#B8B8B8); background-image: -webkit-gradient(linear,left top,left bottom,from(#EBEBEB),to(#B8B8B8)); background-image: -webkit-linear-gradient(top,#EBEBEB,#B8B8B8); background-image: linear-gradient(top,#EBEBEB,#B8B8B8); } #use-lodash { margin-right: 30px; } #content { margin-left: 145px; position: relative; } #content span { height:17px; width:7px; vertical-align:top; display:inline-block; border-left:1px solid #999; font-size:12px; } #content div{ margin:0; font-family:monospace; color: black; height: 23px; } h2 { margin:0; height: 15px; clear:both; font-weight: normal; width:100%; font-size:11px; } #sidebar { height:100%; width: 120px; position:absolute; } #sidebar-free { height: 197px; width: 100%; border: 1px solid #ccc; position:relative; padding: 55px 5px; text-align: center; background-color: #3e5f7c; } .color0 { background-color: #9BFFBB} .color1 { background-color: #E3FF7E} .color2 { background-color: #B9C6FF} .color3 { background-color: #99FF7E} .color4 { background-color: #FFB38A} .color5 { background-color: #A5FCFF} .color6 { background-color: #FF8E9B} .color7 { background-color: #FFE589} .color8 { background-color: #FFA3D8} .color9 { background-color: #5ca6ff} </style> <script> $(document).ready(function(){ var allEvents = $('#allEvents'), divDebounce_true = $('#debounce_true'), divDebouncejtrue = $('#debouncejtrue'), divDebounce_false = $('#debounce_false'), divDebouncejfalse = $('#debouncejfalse'), divThrottle_true = $('#throttle_true'), divThrottlejtrue = $('#throttlejtrue'), divThrottlejfalse = $('#throttlejfalse'), sidebar_mousemove = $('#sidebar-free'), counter = 0, next_color = 0, drawing, drawing_automated, lazy_Debounce_Events, lazyDebounce_true, lazyDebouncejtrue, lazyDebounce_false, lazyDebouncejfalse, lazyThrottle_true, lazyThrottlejtrue, lazyThrottlejfalse; function update(div, color){ div[0].lastChild.className = 'color' + color; div[0].lastChild.innerHTML = color; } function updateEvents(){ update(allEvents, next_color); lazyDebounce_true(divDebounce_true, next_color); lazyDebounce_false(divDebounce_false, next_color); lazyThrottle_true(divThrottle_true, next_color); next_color++; if (next_color > 9){ next_color = 0; } } function setup_lazy_functions(_){ lazy_Debounce_Events = _.throttle(updateEvents, 50); lazyDebounce_true = _.debounce(update, 200, true); lazyDebounce_false = _.debounce(update, 200, false); lazyThrottle_true = _.throttle(update, 200); } // Initially demo it with underscore.js setup_lazy_functions(_); function reset(){ allEvents.html('<span></span>'); divDebounce_true.html('<span></span>'); divDebouncejtrue.html('<span></span>'); divDebounce_false.html('<span></span>'); divDebouncejfalse.html('<span></span>'); divThrottle_true.html('<span></span>'); divThrottlejtrue.html('<span></span>'); divThrottlejfalse.html('<span></span>'); next_color = 0; counter = 0; clearInterval(drawing_automated); clearInterval(drawing); } sidebar_mousemove.on('mousemove', function (){ lazy_Debounce_Events(); }); sidebar_mousemove.on('mouseenter', function(){ reset(); draw(); }); $('#every100').on('click', function(e){ e.preventDefault(); reset(); draw(); drawing_automated = setInterval(function(){ sidebar_mousemove.trigger('mousemove'); }, 100); }); $('#every300').on('click', function(e){ e.preventDefault(); reset(); draw(); drawing_automated = setInterval(function(){ sidebar_mousemove.trigger('mousemove'); sidebar_mousemove.trigger('mousemove'); }, 300); }); $('#use-lodash').on('click', function(e){ e.preventDefault(); if ($(this).data('lodash')){ setup_lazy_functions(_); $(this).data('lodash', false) .html('Using <strong>underscore.js</strong> | <del>lodash.js</del>') } else { setup_lazy_functions(lo); $(this).data('lodash', true) .html('Using <del>underscore.js</del> | <strong>lodash.js</strong>') } }); var draw = function(){ drawing = setInterval(function(){ counter++; allEvents[0].appendChild(document.createElement('span')); divDebounce_true[0].appendChild(document.createElement('span')); divDebouncejtrue[0].appendChild(document.createElement('span')); divDebounce_false[0].appendChild(document.createElement('span')); divDebouncejfalse[0].appendChild(document.createElement('span')); divThrottlejtrue[0].appendChild(document.createElement('span')); divThrottlejfalse[0].appendChild(document.createElement('span')); divThrottle_true[0].appendChild(document.createElement('span')); if (counter > 95){ clearInterval(drawing); clearInterval(drawing_automated); } }, 30); }; }); </script>
注意到上面的运行结果,第一行 Mousemove Events 展示了 mousemove 事件触发的频率。第二行和第三行是分别使用 underscore 与 jQuery 的 debounce 方法后事件的触发频率。第四、五行则是增加了一个 delay 参数后的触发频率。与之对比的是最后三行,使用的是 throttle 方法。
debounce的简单封装
在使用中,如果不用到Underscore.js库,那么我们可以自己封装一个throttle与debounce的实现
/** * * @param fn {Function} 实际要执行的函数 * @param delay {Number} 延迟时间,也就是阈值,单位是毫秒(ms) * * @return {Function} 返回一个“去弹跳”了的函数 */ function debounce(fn, delay) { // 定时器,用来 setTimeout var timer // 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 fn 函数 return function () { // 保存函数调用时的上下文和参数,传递给 fn var context = this var args = arguments // 每次这个返回的函数被调用,就清除定时器,以保证不执行 fn clearTimeout(timer) // 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作), // 再过 delay 毫秒就执行 fn timer = setTimeout(function () { fn.apply(context, args) }, delay) } }
其实思路很简单, debounce 返回了一个闭包,这个闭包依然会被连续频繁地调用,但是在闭包内部,却限制了原始函数 fn 的执行,强制 fn 只在连续操作停止后只执行一次。
debounce 的使用方式如下:
function ajax_lookup( event ) { // 对输入的内容$(this).val()执行 Ajax 查询 }; // 字符输入的频率比你预想的要快,Ajax 请求来不及回复。 $('input:text').keyup( ajax_lookup ); // 当用户停顿250毫秒以后才开始查找 $('input:text').keyup( debounce( ajax_lookup. 250 ) );
throttle的简单封装
/** * * @param fn {Function} 实际要执行的函数 * @param delay {Number} 执行间隔,单位是毫秒(ms) * * @return {Function} 返回一个“节流”函数 */ function throttle(fn, threshhold) { // 记录上次执行的时间 var last // 定时器 var timer // 默认间隔为 250ms threshhold || (threshhold = 250) // 返回的函数,每过 threshhold 毫秒就执行一次 fn 函数 return function () { // 保存函数调用时的上下文和参数,传递给 fn var context = this var args = arguments var now = +new Date() // 如果距离上次执行 fn 函数的时间小于 threshhold,那么就放弃 // 执行 fn,并重新计时 if (last && now < last + threshhold) { clearTimeout(timer) // 保证在当前时间区间结束后,再执行一次 fn timer = setTimeout(function () { last = now fn.apply(context, args) }, threshhold) // 在时间区间的最开始和到达指定间隔的时候执行一次 fn } else { last = now fn.apply(context, args) } } }
原理也不复杂,相比 debounce ,无非是多了一个时间间隔的判断,其他的逻辑基本一致。
throttle 的使用方式如下:
function log( event ) { console.log( $(window).scrollTop(), event.timeStamp ); }; // 控制台记录窗口滚动事件,触发频率比你想象的要快 $(window).scroll( log ); // 控制台记录窗口滚动事件,每250ms最多触发一次 $(window).scroll( throttle( log, 250 ) );
其它实现
下面是 愚人码头 对throttle与debounce的实现
/* * 频率控制 返回函数连续调用时,fn 执行频率限定为每多少时间执行一次 * @param fn {function} 需要调用的函数 * @param delay {number} 延迟时间,单位毫秒 * @param immediate {bool} 给 immediate参数传递false 绑定的函数先执行,而不是delay后后执行。 * @return {function}实际调用函数 */ var throttle = function (fn,delay, immediate, debounce) { var curr = +new Date(),//当前事件 last_call = 0, last_exec = 0, timer = null, diff, //时间差 context,//上下文 args, exec = function () { last_exec = curr; fn.apply(context, args); }; return function () { curr= +new Date(); context = this, args = arguments, diff = curr - (debounce ? last_call : last_exec) - delay; clearTimeout(timer); if (debounce) { if (immediate) { timer = setTimeout(exec, delay); } else if (diff >= 0) { exec(); } } else { if (diff >= 0) { exec(); } else if (immediate) { timer = setTimeout(exec, -diff); } } last_call = curr; } }; /* * 空闲控制 返回函数连续调用时,空闲时间必须大于或等于 delay,fn 才会执行 * @param fn {function} 要调用的函数 * @param delay {number} 空闲时间 * @param immediate {bool} 给 immediate参数传递false 绑定的函数先执行,而不是delay后后执行。 * @return {function}实际调用函数 */ var debounce = function (fn, delay, immediate) { return throttle(fn, delay, immediate, true); };
使用场景
要牵涉到连续事件或频率控制相关的应用都可以考虑到这两个函数,比如:
- input 中输入文字自动发送 ajax 请求进行自动补全: debounce
- resize window 重新计算样式或布局:debounce
- scroll 时更新样式,如随动效果:throttle
最重要的还是理解两者对调用时间及次数上的处理,根据业务逻辑选择最合适的优化方案!
参考
css-tricks
javascript函数的throttle和debounce
本文:Javascript: JS函数节流与防抖 throttle,debounce