原因
节流函数:让一个函数无法在短时间内连续调用,只有当上一次函数执行后,过了规定的时间间隔,才能进行下一次该函数的调用。或者说你在操作的时候不会马上执行该函数,而是等你不操作的时候才会执行。(一定时间内js方法只跑一次。比如人的眨眼睛,就是一定时间内眨一次。)
函数防抖:频繁触发的情况下,只有足够的空闲时间,才执行代码一次。(比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。)
函数节流和函数防抖,两者都是优化高频率执行js代码的一种手段。
影响
函数防抖的影响:防止函数在极短的时间内反复调用,造成资源的浪费。
页面上的某些事件触发频率非常高,比如滚动条滚动、窗口尺寸变化、鼠标移动等,如果我们需要注册这类事件,不得不考虑效率问题,又特别是事件处理中涉及到了大量的操作。
当窗口尺寸发生变化时,哪怕只变化了一点点,都有可能造成成百上千次对处理函数的调用,这对网页性能的影响是极其巨大的。于是,我们可以考虑,每次窗口尺寸变化、滚动条滚动、鼠标移动时,不要立即执行相关操作,而是等一段时间,以窗口尺寸停止变化、滚动条不再滚动、鼠标不再移动为计时起点,一段时间后再去执行操作,就像电梯关门那样。
再考虑一个搜索的场景(例如百度),当我在一个文本框中输入文字(键盘按下事件)时,需要将文字发送到服务器,并从服务器得到搜索结果,这样的话,用户直接输入搜索文字就可以了,不用再去点搜索按钮,可以提升用户体验,类似于下面的效果:
上面的效果,我没有点击搜索按钮,也没有按回车键,只是写了一些搜索的文字而已。
可是如何来实现上面的场景呢?如果文本框的文字每次被改变(键盘按下事件),我都要把数据发送到服务器,得到搜索结果,这是非常恐怖的!想想看,我搜索“google”这样的单词,至少需要按6次按键,就这一个词,我需要向服务器请求6次,并让服务器去搜索6次,但我只需要最后一次的结果就可以了。如果考虑用户按错的情况,发送请求的次数更加恐怖。这样就造成了大量的带宽被占用,浪费了很多资源。
函数节流的影响:有时会为页面绑定 resize 事件,或者为一个页面元素绑定拖拽事件(其核心就是绑定 mousemove),这种事件有一个特点,就是用户不必特地捣乱,他在一个正常的操作中,都有可能在一个短的时间内触发非常多次事件绑定程序。
而DOM 操作时很消耗性能的,这个时候,如果你为这些事件绑定一些操作 DOM 节点的操作的话,那就会引发大量的计算,在用户看来,页面可能就一时间没有响应,这个页面一下子变卡了变慢了。甚至在 IE 下,如果你绑定的 resize 事件进行较多 DOM 操作,其高频率可能直接就使得浏览器崩溃。
避免方式
函数防抖的应用场景
//根据函数防抖思路设计出第一版的最简单的防抖代码:
let timer; // 维护同一个timer
function debounce(fn, delay) {
clearTimeout(timer);
timer = setTimeout(function(){
fn();
}, delay);
}
// 用onmousemove测试一下防抖函数:
function testDebounce() {
console.log('test');
}
document.onmousemove = () => {
debounce(testDebounce, 1000);
}
上面的debounce就是防抖函数,在document中鼠标移动的时候,会在onmousemove最后触发的1s后,执行回调函数testDebounce;
如果我们一直在浏览器中移动鼠标(比如10s),会发现在10 + 1s后才会执行testDebounce函数(因为clearTimeout(timer),每次移动还没来得及执行timer就再次触发debounce,然后clearTimeout(timer))。
在上面的代码中,会出现一个问题,let timer只能在setTimeout的父级作用域中,这样才是同一个timer,并且为了方便防抖函数的调用和回调函数fn的传参问题,我们应该用闭包来解决这些问题。
优化后的代码:
function debounce(fn, delay) {
let timer; // 维护一个 timer
return function () {
let _this = this; // 取debounce执行作用域的this
let args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
fn.apply(_this, args);
//用apply指向调用debounce的对象,相当于_this.fn(args);
}, delay);
}
}
// test
function testDebounce(e, content) {
console.log(e, content);
}
var testDebounceFn = debounce(testDebounce, 1000); // 防抖函数
document.onmousemove = function (e) {
testDebounceFn(e, 'debounce'); // 给防抖函数传参
}
函数节流的应用场景
function throttle(fn, delay) {
let timer;
return function () {
let _this = this;
let args = arguments;
if (timer) {
return;
}
timer = setTimeout(function () {
fn.apply(_this, args);
timer = null;
// 在delay后执行完fn之后清空timer,此时timer为假,
throttle触发可以进入计时器
}, delay)
}
}
function testThrottle(e, content) {
console.log(e, content);
}
let testThrottleFn = throttle(testThrottle, 1000); // 节流函数
document.onmousemove = function (e) {
testThrottleFn(e, 'throttle'); // 给节流函数传参
}
若一直在浏览器中移动鼠标(比如10s),则在这10s内会每隔1s执行一次testThrottle。
函数节流的目的,是为了限制函数一段时间内只能执行一次。因此,定时器实现节流函数通过使用定时任务,延时方法执行。在延时的时间内,方法若被触发,则直接退出方法。从而,实现函数一段时间内只执行一次。
时间戳实现节流函数
function throttle(fn, delay) {
let previous = 0;
// 使用闭包返回一个函数并且用到闭包函数外面的变量previous
return function() {
let _this = this;
let args = arguments;
let now = new Date();
if(now - previous > delay) {
fn.apply(_this, args);
previous = now;
}
}
}
// test
function testThrottle(e, content) {
console.log(e, content);
}
let testThrottleFn = throttle(testThrottle, 1000); // 节流函数
document.onmousemove = function (e) {
testThrottleFn(e, 'throttle'); // 给节流函数传参
}
通过比对上一次执行时间与本次执行时间的时间差与间隔时间的大小关系,来判断是否执行函数。若时间差大于间隔时间,则立刻执行一次函数。并更新上一次执行时间。