javascript函数节流和函数去抖
在underscore
这个库中提供了两个关于控制函数执行频率的方法, throttle
和debounce
。
throttle
和debounce
是解决请求和响应速度不匹配问题的两个方案。差异在于选择不同的策略。
比如生活中的最常见的电梯, 分别用这两种策略解释下:
- throttle: 开门按钮按下 -> 有人进来 -(等10秒, 不管有没有人进来)-> 准时关门运行
- debounce: 开门按钮按下 -> 有人进来, 关门按钮按下 -(开始计时10秒)-> 开门按钮按下, 又有人进来, 关门按钮按下 -> 重新计时 -(开始计时10秒) -> 10秒时间到, 关门运行
如果我们需要做一个过滤的功能,类似于下面这个效果
throttle
我们需要给input绑定一个keyup事件,然后根据它的value操作页面或者过滤数据重新渲染页面,但是在数据比较多的时候, 如果在keyup里面不做一定限制的话, 在性能方面就会有一些影响, 这时候, 就需要"函数节流"这个东西, 限制在多少秒内触发一次某个函数。
就拿我们上面的效果来说:
// HTML
<input type="text" id="input" />
<ul id="ul">
<li>111111111</li>
<li>22222222</li>
<li>333333</li>
<li>4444</li>
<li>1234</li>
<li>5678</li>
<li>9999</li>
<li>6789</li>
<li>01234</li>
</ul>
// javascript
window.onload = function() {
var input = document.getElementById("input");
var ul = document.getElementById("ul");
var li = ul.getElementsByTagName("li");
var len = li.length;
var value, timeout = null;
input.onkeyup = function(ev) {
clearTimeout(timeout);
timeout = setTimeout(function() {
clearTimeout(timeout);
value = ev.target.value.trim();
for (var i = 0; i < len; i++) {
if (li[i].innerHTML.indexOf(value) > -1) {
li[i].style.display = "block";
} else {
li[i].style.display = "none";
}
}
}, 200);
};
};
下面可以把这个函数再次进行封装:
/**
* 函数节流
* @param {Function} fn [要执行的函数]
* @param {Number} delay [连续执行间隔]
* @param {Number} mustRunDelay [隔多久至少执行一次]
* @return {Function}
*/
function throttle(fn, delay, mustRunDelay) {
var timer = null;
var start;
return function() {
var args = arguments,
curr = +(new Date());
clearTimeout(timer);
if (!start) {
start = curr;
}
if (curr - start >= mustRunDelay) {
fn.apply(null, args);
// 重置上次执行时间戳
start = curr;
} else {
timer = setTimeout(function() {
fn.apply(null, args);
}, delay);
}
};
};
封装完之后, 我们这样改下我们的代码:
input.onkeyup = throttle(function(e) {
// ...
}, 300, 400);
在平时可以有好多地方用到throttle
,比如浏览器的resize,鼠标滚轮事件等等。
debounce
说完了throttle
, 我们再来看看debounce
的应用场景, 还是用刚才上面的场景做示例:
// HTML
<input type="text" id="input" />
<ul id="ul">
<li>111111111</li>
<li>22222222</li>
<li>333333</li>
<li>4444</li>
<li>1234</li>
<li>5678</li>
<li>9999</li>
<li>6789</li>
<li>01234</li>
</ul>
// javascript
window.onload = function() {
var input = document.getElementById("input");
var ul = document.getElementById("ul");
var li = ul.getElementsByTagName("li");
var len = li.length;
var value, timeout = null;
input.onkeyup = function(ev) {
// 注意这里
if(timeout) {
return;
}
// 注意这里
timeout = setTimeout(function() {
clearTimeout(timeout);
timeout = null;
value = ev.target.value.trim();
for (var i = 0; i < len; i++) {
if (li[i].innerHTML.indexOf(value) > -1) {
li[i].style.display = "block";
} else {
li[i].style.display = "none";
}
}
}, 200);
};
};
和上面的例子一对比, 发现改动的地方很少, 就是判断上个timeout
的句柄是否存在(上一次处理函数还没有执行), 存在就不往下走, 如果不存在, 延时200
毫秒后执行处理函数, 并清除本次timeout
的句柄, 这种情况我们在很多地方也会遇到, 比如我们要在一个输入框, 在我们最后一次输入之后300
毫秒一次异步请求去根据输入的值获取数据。
和throttle
一样, 我们一起来封装个debounce
, 以便后面调用:
/**
* [函数去抖]
* @param {Function} fn [要执行的函数]
* @param {Number} delay [最后一次事件处理函数在多少毫秒之后被触发]
*/
function debounce(fn, delay) {
if (fn.timeoutHandler) {
return;
}
fn.timeout = setTimeout(function() {
clearTimeout(fn.timeoutHandler);
fn.timeoutHandler = null;
fn();
}, delay);
}
总结
throttle
和debounce
都可以在性能优化方面, 所以在遇到一些频繁操作时(比如鼠标连续点击, 输入框输入, 滚动事件), 考虑到这块会对浏览器或者服务器减轻一些压力, 当然具体用哪个方法还得看具体的业务场景。