节流函数
节流函数用于控制函数执行的频率.比如用户设置时间间隔为1秒,那么在1s的时间内函数只执行一次.
如果上面的表述还不清楚,通过下面的案例来深入理解.
不加节流函数
window.onscroll = function (value) {
console.log('移动了鼠标');
};
从上面可以看出,不加节流函数时,滑动鼠标触发事件的次数是惊人的.如果被执行的函数里面包含了复杂的计算逻辑,在这么高的调用频率下,会造成严重的页面卡顿.
增加节流函数
window.onscroll = throttle(function (value) {
console.log('移动了鼠标');
}, 1000);
节流函数将时间间隔设置为1000毫秒,不管怎么滑动,每隔一秒钟才会执行一次函数
节流函数不光在面试中会经常遇到,在实际的业务场景里也有广泛的应用.比如拖拽事件,GPS获取地理位置等.
借鉴underscore中的源码,下面由简入繁,一步步去实现一遍节流函数的编写.
版本1
从throttle函数的返回值可以看出来,传入原始函数,它会返回一个新函数,新函数和原始函数相比,只多了一个节流功能,其他与原始函数保持一致.
第一版先返回一个新函数,在新函数内调用原始函数.
const fun = throttle(function (value) {
console.log(value);
});
fun(123);
fun(456);
fun(789);
function throttle(fn) {
return function () {
fn.apply(null, arguments);
};
}
版本2
传入时间间隔1000毫秒,期待每过1秒钟执行一次原始函数.
const fun = throttle(function (value) {
console.log(value);
}, 1000);
fun(123);
fun(456);
fun(789);
/**
*
* @param {*} fn
* @param {*} wait 延迟时间
*
*/
function throttle(fn, wait) {
let last_time = 0,args;
let timer;
return function () {
let now_time = new Date().getTime(); //now_time本次运行该函数时的时间戳
let remain = wait - (now_time - last_time); //last_time上次运行该函数的时间戳
args = arguments;
if (remain <= 0) {
//已经到了间隔时间,可以运行该函数
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(null, args);
last_time = now_time;
} else if (!timer) {
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
fn.apply(null, args);
last_time = now_time;
}, remain);
}
};
}
123被立刻打印出,过了1秒钟789被打印.456没有被打印
每次调用增强后的函数时,会将本次运行的时间戳(now_time)和上一次运行的时间戳(last_time)的差值与wait进行比较,如果还在间隔时间内,就设置定时器等待执行.如果过了间隔时间就立即执行.
fun(123);fun(456);fun(789);这三句代码是同步执行的,123会立即执行输出,456由于还在间隔时间的限制内,被设置成了定时器延缓执行.789仍然在间隔时间的限制内,但已经存在了定时器,因此这次操作会被舍弃掉,但是它仍然把args更新为789,因此最后定时器函数执行时输出的就是789
将上面重复代码的部分封装成exec函数,如下:
/**
*
* @param {*} fn
* @param {*} wait 延迟时间
*
*/
function throttle(fn, wait) {
let last_time = 0,args;
let timer;
function exec(args) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(null, args);
last_time = new Date().getTime();
}
return function () {
let now_time = new Date().getTime(); //now_time本次运行该函数时的时间戳
let remain = wait - (now_time - last_time); //last_time上次运行该函数的时间戳
args = arguments;
if (remain <= 0) {
//已经到了间隔时间,可以运行该函数
exec(args);
} else if (!timer) {
timer = setTimeout(() => {
exec(args);
}, remain);
}
};
}
版本3
当前函数已经具备基础的节流功能了,但在有些应用场景下期待节流函数第一次不执行或者最后一次不执行.
在时间参数后面再加一个配置项,将节流函数的功能继续完善:
leading设置为false时:第一次不立即执行,而是延时后再执行.
trailing设置为false是:第一次会立即执行,最后一次被延时的函数不执行.
const fun = throttle(
function (value) {
console.log(value);
},
1000,
{
leading: false,
}
);
fun(123);
fun(456);
fun(789);
/**
*
* @param {*} fn
* @param {*} wait 延迟时间
*
*/
function throttle(fn, wait, params) {
let last_time = 0,args;
let timer;
if (params == null) {
params = {};
}
function exec(args) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(null, args);
last_time = new Date().getTime();
}
return function () {
let now_time = new Date().getTime(); //now_time本次运行该函数时的时间戳
if (!timer && params.leading === false) {
//让第一次不立即执行,延迟后再执行
last_time = now_time;
}
let remain = wait - (now_time - last_time); //last_time上次运行该函数的时间戳
args = arguments;
if (remain <= 0) {
//已经到了间隔时间,可以立即执行该函数
exec(args);
} else if (!timer && params.trailing !== false) { //trailing控制处于间隔时间内的延时函数不执行
//延迟执行
timer = setTimeout(() => {
exec(args);
}, remain);
}
};
}
params.leading为false时,界面延迟1秒后再输出789,不会再输出其他内容.
params.trailing为false时,界面立即输出123,不会再输出其他内容.
通过 !timer && params.leading === false 判断第一次是否立即执行.如果为true,那么last_time被赋予now_time,remian一定大于0,因此第一次就会延迟执行.
版本4(最终版)
如果原始函数有返回值,那么期待增强后的节流函数也能有返回值,继续改进.
const fun = throttle(function (value) {
return value;
}, 1000);
console.log(fun(123));
console.log(fun(456));
console.log(fun(789));
/**
*
* @param {*} fn
* @param {*} wait 延迟时间
*
*/
function throttle(fn, wait, params) {
let last_time = 0,args;
let timer, result;
if (params == null) {
params = {};
}
function exec(args) {
if (timer) {
clearTimeout(timer);
timer = null;
}
result = fn.apply(null, args);
last_time = new Date().getTime();
}
return function () {
let now_time = new Date().getTime(); //now_time本次运行该函数时的时间戳
if (!timer && params.leading === false) {
//让第一次不立即执行,延迟后再执行
last_time = now_time;
}
let remain = wait - (now_time - last_time); //last_time上次运行该函数的时间戳
args = arguments;
if (remain <= 0) {
//已经到了间隔时间,可以立即执行该函数
exec(args);
} else if (!timer && params.trailing !== false) {
//延迟执行
timer = setTimeout(() => {
exec(args);
}, remain);
}
return result;
};
}
函数内添加一个result变量用来存储返回值.当输入为123的函数运行,将结果存储到result中.输入为456和789的函数因为还在间隔时间内不能执行函数,只能返回上一次计算的result值,因此也为123.
防抖函数
防抖函数也是用于控制函数执行的频率,不过它与节流函数不一样.它是在间隔时间内监测到再没有外部函数调用时才开始执行一次,借助下面的案例来加深理解.
不加防抖函数
const user_name = document.getElementById('user_name');
user_name.oninput = function () {
console.log(`输入了${user_name.value},发送给后台模糊查询`);
};
不加防抖函数时,输入一个字符就会执行一次函数.假如这是一个模糊搜索产品名称的输入框,每输入一个字符就向后端发送一个ajax请求,势必会造成性能上的大量损耗.
加了防抖函数
const user_name = document.getElementById('user_name');
user_name.oninput = debounce(function () {
console.log(`输入了${user_name.value},发送给后台模糊查询`);
}, 500);
加了防抖函数后神奇的发现,用户在输入过程中是不会执行函数的.直到不再输入后再等待500毫秒才会执行一次.
版本1
传入时间参数500毫秒,期待500毫秒内没有监听到外部的函数调用后才执行函数.
const user_name = document.getElementById('user_name');//页面的input
user_name.oninput = debounce(
function () {
console.log(`输入了${user_name.value},发送给后台模糊查询`);
},
500
);
function debounce(fn, wait) {
let timer, args, result;
let last_time = 0;
function later() {
clearTimeout(timer);
timer = null;
const now_time = new Date().getTime();
const last = now_time - last_time;
if (last < wait) {
timer = setTimeout(later, wait - last);
} else {
result = fn.apply(null, args);
}
}
return function () {
args = arguments;
last_time = new Date().getTime();
if (!timer) {
timer = setTimeout(later, wait);
}
return result;
};
}
外部每一次调用函数时,都会将当前的时间戳赋予last_time.在延迟函数里,通过now_time和last_time的差值与wait进行比较,可以判断有没有处于间隔期内,如果还在间隔期就重新设定定时器延迟执行.如果过了间隔期仍然没有外部调用时就可以执行函数了.
版本2(最终版)
调用时加入第三个参数immediate,当其为true时期待首次调用立即执行,后续在间隔时间内调用都不执行.
其实就是在上面的逻辑里,把函数的执行放到了最前面.
const user_name = document.getElementById('user_name');
user_name.oninput = debounce(
function () {
console.log(`输入了${user_name.value},发送给后台模糊查询`);
},
500,
true
);
function debounce(fn, wait, immediate) {
let timer, args, result, call_now;
let last_time = 0;
function later() {
const now_time = new Date().getTime();
const last = now_time - last_time;
if (last < wait) {
timer = setTimeout(later, wait - last);
} else {
clearTimeout(timer);
timer = null;
if (!immediate) {
result = fn.apply(null, args);
}
}
}
return function () {
args = arguments;
call_now = immediate && !timer; //是否立即执行
last_time = new Date().getTime();
if (!timer) {
timer = setTimeout(later, wait);
}
if (call_now) {
result = fn.apply(null, args);
}
return result;
};
}