第 3 题:什么是防抖和节流?有什么区别?如何实现?
防抖
触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间,思路:每次触发事件时都取消之前的延时调用方法
function debounce(fn) {
let timeout = null; // 创建一个标记用来存放定时器的返回值
return function () {
clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
fn.apply(this, arguments);
}, 500);
};
}
function sayHi() {
console.log('防抖成功');
}
var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖
节流
高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率,思路:每次触发事件时都判断当前是否有等待执行的延时函数
function throttle(fn) {
let canRun = true; // 通过闭包保存一个标记
return function () {
if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
canRun = false; // 立即设置为false
setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments);
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
canRun = true;
}, 500);
};
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(49)
canRun和timeout的定义应该放到方法外,不然延时到了还是会执行多次
注意this指向问题。
如果单单为了打印那句console.log('防抖成功');确实可以直接fn(),但我们得考虑实际情况,让sayHi的this指向input是必要的,例如我们需要在输入完改变字体颜色,如下:
function sayHi() { console.log('防抖成功'); this.style.color = 'red'; }
这个时候fn.apply(this, arguments);的作用就显而易见了
防抖:动作绑定事件,动作发生后一定时间后触发事件,在这段时间内,如果该动作又发生,则重新等待一定时间再触发事件。
节流: 动作绑定事件,动作发生后一段时间后触发事件,在这段时间内,如果动作又发生,则无视该动作,直到事件执行完后,才能重新触发。
如果demo一中的sayHi()方法其实,没有什么区别
但是如果sayHi这个callback要改变this的指向,或者要更方便的传参的话用apply就比较方便
用call或bind也可以
这里引申的话会有俩经常会聊到的问题
1,call,apply,bind的区别
2,this的指向
这俩应该是面试必聊的问题,可以好好整理一下。博主的这个面试题的系列中这俩都有说到。
不加apply,sayHi里面this肯定是指向window的,但是加上apply后,
fn.apply(this, arguments)
这段代码里面的this的指向就要分情况讨论了,而且这个this就是sayHi里面的this。这里的情况其实指的就是setTimeout里面的回调函数是普通函数还是箭头函数。如果是箭头函数,则这里的this最终指向的是input对象,如果为普通函数,this则指向window。setTimeout关于this的问题 | MDN,箭头函数 | MDN2. 普通函数表现
3. 解决办法
这里似乎有个问题,就是如果使用定时器的话,在 500ms 后执行的始终是前 500ms 内触发的第一个函数 fn,之后的在 500ms 内触发函数都将被丢弃,这样的话,fn 里获取的参数 arguments 可能不准确。应该以 500ms 内触发的最后一个函数为准,而不是第一个函数。
防抖添加个
immediate
参数,控制直接触发还是最后触发防抖:
在掘金上看到的,感觉不错 https://juejin.im/entry/58c0379e44d9040068dc952f
防抖 :
节流
@Liubasara
异步情况下这样应该就好了
虽然二者都有延迟当前动作的反馈,但是防抖的延迟时间是确定的,延迟周期内如果有新动作进入,旧的动作将会被取消。
而节流是提前设置了一个阀门,只有当阀门打开的时候,该动作才有机会执行。如果阀门是关闭的,那这个动作就不会进入执行区。个人理解防抖是后置的处理高频事件方式,而节流是前置处理。防抖机制隐含了一个优先级的概念,后到的先执行,因此事件的进入事件越晚优先级实则越高,而优先级最高的具备执行权,而进入时间这个准入条件是不由开发者提前预设的,事件的执行更加离散无规则。而缓冲机制并没有为事件分配权重,只是设置了一个均匀频率的信号量,该信号量的开启和关闭是决定能否进入执行区的条件,而与事件无关,准入条件是人为设置的,相对来说执行更规律。
防抖节流还是推荐冴羽大大的gitbub blog
不行的,因为bind方法返回一个新的函数并将这个函数绑定到this上,但并不会执行,这里需要执行fn
在CSS-tricks发现了下面的链接,这个应该算是debounce的根儿了。文中作者给出了一个每行都带注释的版本,有兴趣的小伙伴可以研究下。
Debouncing Javascript Methods
为了给fn传参
这里主要是因为在多次的异步操作之后,可能会出现指针丢失的情况,因为每一次异步操作的时候都会创建一个新的任务,新的任务的执行上下文可能会发生改变。所以需要将当前的指针和执行上下文传递下去
手写函数防抖和节流、应用场景、比较
箭头函数白用了
添加
immediate
可以分开到外边的吧?在处理返回节流方法就判断,不用后边还要每次执行都多做一次判断节流函数 - 点击之后立即执行函数
https://juejin.cn/post/6914591853882900488 逐步分析了防抖与节流的实现,以及this指向、传参问题,望指点
闲来无事,就写一段好了
//防抖
function debounce(fn,wait){
var timer=null;
return function(...args){
if(timer) clearTimeout( timer);
timer=setTimeout(()=>{
fn.apply(this,args);
},wait)
}
}
//节流
function throttle(fn,wait){
var prev=0;
return function(...args){
var now=new Date().getTime();
if(now-prev>wait){
fn.apply(this,args);
prev=now;
}
}
}
// 防抖
// 节流
防抖应该是高频事件被触发n秒后再执行回调吧?
说的太笼统了,setTimeout的this指的是window,而箭头函数指的不是setTimeout的this是function的this,都是window对象,所以这个apply没有什么意思。
用箭头函数的目的是为了让fn.apply的this和arguments都是闭包return的函数的this和arguments。
下面应该是常用场景的代码,可以参考这个代码思考实现
其实对于节流我有个疑问,最后一次回调是否应该必须触发。
当前的实现可能存在最后一次不触发的情况。
假设 time 是相对于第一次触发的时间差
我优化了一下节流的逻辑,保证最后一次必须执行。
估计是改变this指向
这里用了apply确实使得this指向了input对象;对于“因为 sayHi 函数定义在全局中,所以调用时里面this指向window”,测试了一下直接使用fn(arguments)的话,在sayHi中打印this为undefined;js中this是在运行时绑定的,而不是定义时绑定的
有个问题,假如传入的方法是异步的,上述的节流方法是没用的啊,考虑把
fn.apply(this, arguments)
这一句放在setTimeout外面是不是会好一点?就像下面这样。@Liubasara 是的,应该改为「因为 sayHi 函数是在全局中运行,所以this指向了window」,不过你说的「测试了一下直接使用fn(arguments)的话,在sayHi中打印this为undefined」是不对的哦,不显示绑定,是这里是指向window的。截图如下:
楼上大佬说的是对的,但是要注意这里的this(input)是addEventListener中调用回调的时候传进来的,这和是不是箭头函数没关系。
另外,因为不确定入参的数量,所以利用apply还可以传入扩展后的arguments(如果不兼容...arguments语法的话)。
已上。
@KouYidong 节流函数有点问题,第一次应该是立即执行,而不是delay 500ms后再执行
请问防抖那里可以写成
setTimeout(fn.bind(this), 500)
吗(小白的疑问)
@Carrie999 为了保证sayHi执行时的this指向input
@zhongtingbing 你去试试在 不加 apply 时去 sayHi 函数里打印下 this看看什么
是指向window的。因为 sayHi 函数是在全局中调用运行,所以 this 指向了 window,所以才需要加上 apply,显示绑定 this 值(input对象)到 sayH 函数里面去
@zhongtingbing
加上 apply 确保 在 sayHi 函数里的 this 指向的是 input对象(不然就指向 window 了,不是我们想要的)。
这里的箭头函数依旧是指向 input 对象。
请问为甚么你要确保fn执行的上下文是this?在这个箭头函数里this又是指向的谁?
之前有看过一步步实现的文章,如下:
@Carrie999 call 和 apply 可以了解一下
@Carrie999 关键在第一个参数,为了确保上下文环境为当前的this,所以不能直接用fn。
请问,为什么要 fn.apply(this, arguments);而不是这样 fn()