函数节流和防抖
函数的高阶使用2
我们来看一个分析:
如果要实现一个拖拽功能,需要一路监听 mousemove 事件,在回调中获取元素当前位置,然后重置dom的位置来进行样式改变。如果不加以控制,每移动一定像素而触发的回调数量非常惊人,回调中又伴随着 DOM 操作,继而引发浏览器的重排与重绘,性能差的浏览器可能就会直接假死.
在某些情况下会引发函数被非常频繁地调用,而造成大的性能问题。解决性能问题的处理办法就是函数节流和函数防抖。
函数防抖
是函数在特定的时间内不被再调用后执行。 也就是让某个函数在上一次执行后,满足等待某个时间内不再触发此函数后再执行,而在这个等待时间内再次触发此函数,等待时间会重新计算。
应用场景
- 用户注册时候手机号码验证和邮箱验证只有等到用户输入完毕后,前端才需要检查格式是否正确,如果不正确,再弹出提示语。
- scroll/resize事件
- 文本连续输入,ajax验证/关键字搜索'
实现方式
- 函数防抖的要点,也是需要一个setTimeout来辅助实现。延迟执行需要跑的代码。
- 如果方法多次触发,则把上次记录的延迟执行代码用clearTimeout清掉,重新开始。
- 如果计时完毕,没有方法进来访问触发,则执行代码。
举个例子: 在移动鼠标时触发打印函数 加上函数防抖。
var timer = false;
document.querySelector("#div").mousemove = function(){
clearTimeout(timer); // 当事件触发的时候,清除之前等待执行的函数,
timer = setTimeout(function(){ // 开启新的延时执行函数
console.log("函数防抖");
}, 300);
};
我们现在对上述方法封装一下
// 函数防抖
const debounce = function(fn, wait=300){
return function(){
clearTimeout(fn.timer); // 当事件触发的时候,清除之前等待执行的函数,
fn.timer = setTimeout(fn, 300);
}
}
但是我们就会发现 fn 的this 指向发生了改变, fn参数是接收不到的。
function debounce = function(fn, wait=300){
return function(){
clearTimeout(fn.timer); // 当事件触发的时候,清除之前等待执行的函数,
fn.timer = setTimeout(fn.bind(this), wait);
}
}
或者使用apply
function debounce = function(fn, wait=100){
return function(){
clearTimeout(fn.timer); // 当事件触发的时候,清除之前等待执行的函数,
fn.timer = setTimeout( ()=>{ // 不能写匿名函数,this会发生改变
return fn.apply(this, arguments);
}, wait);
}
}
函数节流
函数节流,即限制函数的执行频率,在持续触发事件的情况下,间断地执行函数。只要当前函数没有执行完成,任何新触发的函数都会被忽略。就是在固定的时间间隔内函数只会被调用一次。
适用场景
- 频繁的mousemove/keydown,比如高频的鼠标移动,游戏射击类的
- 搜索联想: 监听keypress事件,然后异步去查询结果. 如果快速输入过多字符串, 就会触发高频请求。
- 监听滚动事件判断是否到页面底部自动加载更多(scroll事件)
实现简易
- 函数节流的要点是,声明一个变量当标志位,记录当前代码是否在执行。
- 如果空闲,则可以正常触发方法执行。
- 如果代码正在执行,则取消这次方法执行,直接return。
var timer = false;
document.querySelector("#div").mousemove = function(){
if(timer)
return;
timer = setTimeout(function(){ // 正在执行
console.log("函数节流");
timer = false; // 表示不在执行
}, 100);
};
在来改写一下:
const throttle = function(fn, wait=100){
return function(){
if(fn.timer){ return; }
fn.timer = setTimeout(()=>{
fn.apply(this,arguments);
fn.timer = false;
},wait)
}
}
总结
函数的节流和函数的去抖都是通过减少实际逻辑处理过程的执行来提高事件处理函数运行性能的手段,并没有实质上减少事件的触发次数。
某些函数确实是用户主动调用的,但因为一些客观的原因,这些函数会严重地影响页面性能。解决方案:例如分页技术和延迟加载,都可以避免在页面上同时加载过多数据,造成的页面卡顿。甚至假死现象。其实,优化就是合理利用性能。
underscore v1.7.0
相关的源码实现
_.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 = 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;
};
};