第 3 题:什么是防抖和节流?有什么区别?如何实现?

发布于 2022-09-19 08:02:18 字数 1303 浏览 238 评论 49

防抖

触发高频事件后 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(49

无边思念无边月 2022-05-04 13:57:51

canRun和timeout的定义应该放到方法外,不然延时到了还是会执行多次

云之铃。 2022-05-04 13:57:51

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()

注意this指向问题。

所有深爱都是秘密 2022-05-04 13:57:51

如果单单为了打印那句console.log('防抖成功');确实可以直接fn(),但我们得考虑实际情况,让sayHi的this指向input是必要的,例如我们需要在输入完改变字体颜色,如下:
function sayHi() { console.log('防抖成功'); this.style.color = 'red'; }
这个时候fn.apply(this, arguments);的作用就显而易见了

池予 2022-05-04 13:57:51

防抖:动作绑定事件,动作发生后一定时间后触发事件,在这段时间内,如果该动作又发生,则重新等待一定时间再触发事件。

  function debounce(func, time) {
    let timer = null;
    return () => {
      clearTimeout(timer);
      timer = setTimeout(()=> {
        func.apply(this, arguments)
      }, time);
    }
  }

节流: 动作绑定事件,动作发生后一段时间后触发事件,在这段时间内,如果动作又发生,则无视该动作,直到事件执行完后,才能重新触发。

  function throtte(func, time){
    let activeTime = 0;
    return () => {
      const current = Date.now();
      if(current - activeTime > time) {
        func.apply(this, arguments);
        activeTime = Date.now();
      }
    }
  }
霓裳挽歌倾城醉 2022-05-04 13:57:51

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()

如果demo一中的sayHi()方法其实,没有什么区别
但是如果sayHi这个callback要改变this的指向,或者要更方便的传参的话用apply就比较方便
用call或bind也可以

这里引申的话会有俩经常会聊到的问题
1,call,apply,bind的区别
2,this的指向
这俩应该是面试必聊的问题,可以好好整理一下。博主的这个面试题的系列中这俩都有说到。

刘备忘录 2022-05-04 13:57:51

@zhongtingbing 你去试试在 不加 apply 时去 sayHi 函数里打印下 this看看什么

是指向window的。因为 sayHi 函数定义在全局中,所以调用时里面this指向window,
所以才需要加上 apply,显示绑定 this 值(input对象)到 sayH 函数里面去

不加apply,sayHi里面this肯定是指向window的,但是加上apply后,fn.apply(this, arguments)这段代码里面的this的指向就要分情况讨论了,而且这个this就是sayHi里面的this。这里的情况其实指的就是setTimeout里面的回调函数是普通函数还是箭头函数。如果是箭头函数,则这里的this最终指向的是input对象,如果为普通函数,this则指向window。setTimeout关于this的问题 | MDN箭头函数 | MDN

  1. 箭头函数表现

箭头函数表现

2. 普通函数表现

普通函数表现

3. 解决办法

解决办法

九八野马 2022-05-04 13:57:51

这里似乎有个问题,就是如果使用定时器的话,在 500ms 后执行的始终是前 500ms 内触发的第一个函数 fn,之后的在 500ms 内触发函数都将被丢弃,这样的话,fn 里获取的参数 arguments 可能不准确。应该以 500ms 内触发的最后一个函数为准,而不是第一个函数。

阳光下的泡沫是彩色的 2022-05-04 13:57:51

防抖添加个 immediate 参数,控制直接触发还是最后触发

export function debounce(func: , wait = 500, immediate = true) {
  let timeout, context, args;
  const later = () => setTimeout(() => {
    timeout = null;
    if (!immediate) {
      func.apply(context, args)
    }
    context = args = null;
  }, wait)

  return function(this, ...params) {
    context = this;
    args = params;
    if (timeout) {
      clearTimeout(timeout);
      timeout = later();
    } else {
      timeout = later();
      if (immediate) {
        func.apply(context, args);
      }
    }
  }
}
冷了相思 2022-05-04 13:57:51

防抖:

当一次事件发生后,事件处理器要等一定阈值的时间,如果这段时间过去后 再也没有 事件发生,就处理最后一次发生的事件。假设还差 0.01 秒就到达指定时间,这时又来了一个事件,那么之前的等待作废,需要重新再等待指定时间。

function debounce(fn, wait = 50, immediate) {
  let timer;
  return () => {
    if (immediate) {
      fn.apply(this, arguments)
    }
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, arguments)
    }, wait)
  }
}
拥抱影子 2022-05-04 13:57:51

在掘金上看到的,感觉不错 https://juejin.im/entry/58c0379e44d9040068dc952f

青春如此纠结 2022-05-04 13:57:51

防抖 :

const deb = (fn, delay, immediate) => {
	let timer = null
	return function() {	
		const context = this
		timer && clearTimeout(timer)
		if (immediate) {
			!timer && fn.apply(context, arguments)
		}
		timer = setTimeout(() => {
                       fn.apply(context, arguments)
                }, delay)
	}
}

节流

const throttle = (fn, delay = 2000) => {
	let timer = null
	let startTime = new Date()
	return function() {
		const context = this
		let currentTime = new Date()
		clearTimeout(timer)
		if (currentTime - startTime >= delay) {
			fn.apply(context, arguments)
			startTime = currentTime
		} else {
			//让方法在脱离事件后也能执行一次
			timer = setTimeout(() => {
				fn.apply(context, arguments)
			}, delay)
		}
	}
}
听风念你 2022-05-04 13:57:51

@Liubasara

setTimeout(async () => {
   await fn.apply(this, arguments)
   canRun = true
}, time)

异步情况下这样应该就好了

吹梦到西洲 2022-05-04 13:57:51

虽然二者都有延迟当前动作的反馈,但是防抖的延迟时间是确定的,延迟周期内如果有新动作进入,旧的动作将会被取消。
而节流是提前设置了一个阀门,只有当阀门打开的时候,该动作才有机会执行。如果阀门是关闭的,那这个动作就不会进入执行区。个人理解防抖是后置的处理高频事件方式,而节流是前置处理。防抖机制隐含了一个优先级的概念,后到的先执行,因此事件的进入事件越晚优先级实则越高,而优先级最高的具备执行权,而进入时间这个准入条件是不由开发者提前预设的,事件的执行更加离散无规则。而缓冲机制并没有为事件分配权重,只是设置了一个均匀频率的信号量,该信号量的开启和关闭是决定能否进入执行区的条件,而与事件无关,准入条件是人为设置的,相对来说执行更规律。

狂之美人 2022-05-04 13:57:51

防抖节流还是推荐冴羽大大的gitbub blog

灼痛 2022-05-04 13:57:51

请问防抖那里可以写成
setTimeout(fn.bind(this), 500)
吗(小白的疑问)

不行的,因为bind方法返回一个新的函数并将这个函数绑定到this上,但并不会执行,这里需要执行fn

说不完的你爱 2022-05-04 13:57:51
// 防抖:短时间内大量触发同一事件,只会执行一次函数
function debounce(fn, time){
  let timer = null
  return function(){
    let context = this // 放里面, 符合用户调用习惯
    let args = [...arguments] 
    if(timer){
      clearTimeout(timer)
      timer = null
    }
    timer = setTimeout(()=>fn.apply(context, args), time)
  }
}


// 节流: 在指定时间内只执行一次

// 定时器方案
function throttle(fn, time){
  let timer = null, first = true
  return function(){
    const context = this
    const args = [...arguments]
    if(first){ // 第一次执行
      first = false;
      fn.call(context, args)
    }
    if(!timer){
      timer = setInterval(() => {
        fn.apply(this, args)
        timer = null
        clearInterval(timer)
      }, time)
    }
  }
}


// 时间戳方案
function throttle(fn,wait){
  var pre = Date.now();
  return function(){
      var context = this
      var args = [...arguments]
      var now = Date.now();
      if( now - pre >= wait){
          fn.apply(context,args);
          pre = Date.now(); // 更新初始时间
      }
  }
}
静待花开 2022-05-04 13:57:51

在CSS-tricks发现了下面的链接,这个应该算是debounce的根儿了。文中作者给出了一个每行都带注释的版本,有兴趣的小伙伴可以研究下。
Debouncing Javascript Methods

何处潇湘 2022-05-04 13:57:51

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()

为了给fn传参

七堇年。 2022-05-04 13:57:51

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()

为了给fn传参

这里主要是因为在多次的异步操作之后,可能会出现指针丢失的情况,因为每一次异步操作的时候都会创建一个新的任务,新的任务的执行上下文可能会发生改变。所以需要将当前的指针和执行上下文传递下去

二手情话 2022-05-04 13:57:51
const debounce = (fn, delay) => {
  let time = null
  return (...rest) => {
    clearInterval(time)
    time = setTimeout(() => fn(...rest), delay)
  }
}

const throttle = (fn, delay) => {
  let lock = false;
  return (...rest) => {
    if(lock) return;
    lock = true;
    setTimeout(() => {
      fn(...rest);
      lock = false;
    }, delay)
  }
}
最单纯的乌龟 2022-05-04 13:57:51

@Carrie999 关键在第一个参数,为了确保上下文环境为当前的this,所以不能直接用fn。

箭头函数白用了

∞白馒头 2022-05-04 13:57:51

immediate

添加 immediate 可以分开到外边的吧?在处理返回节流方法就判断,不用后边还要每次执行都多做一次判断

盗梦空间 2022-05-04 13:57:51

节流函数 - 点击之后立即执行函数

const throttle = (fn, wait = 500) => {
    let lock = false;
    return function(...args) {
        if (lock) {
            return;
        }
        lock = true;
        fn.apply(this, args);
        setTimeout(() => {
            lock = false;
        }, wait);
    };
};
锦欢 2022-05-04 13:57:51

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()

https://juejin.cn/post/6914591853882900488 逐步分析了防抖与节流的实现,以及this指向、传参问题,望指点

源来凯始玺欢你 2022-05-04 13:57:51

闲来无事,就写一段好了
//防抖
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;
}
}
}

披肩女神 2022-05-04 13:57:51

// 防抖

function debounce(fn, time, options = { leading: false }) {
  const leading = !!options.leading

  let result
  let timeout
  let lastDate
  return function (...args) {
    let nowDate = +new Date()
    const remainTime = time + lastDate - nowDate

    if ((remainTime <= 0 || !lastDate) && leading) {
      lastDate = nowDate
      result = fn(...args)
    }
    else {
      if (timeout) clearTimeout(timeout)
      timeout = setTimeout(() => {
        result = fn(...args)
      }, time)
    }
    return result
  }
}

// 节流

function throttle(fn, time) {
  return debounce(fn, time, { leading: true })
}
剩余の解释。 2022-05-04 13:57:51
  1. 防抖

触发高频事件后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)); // 防抖
  1. 节流

高频事件触发,但在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));

防抖应该是高频事件被触发n秒后再执行回调吧?

耀眼的星火 2022-05-04 13:57:51

@Carrie999 关键在第一个参数,为了确保上下文环境为当前的this,所以不能直接用fn。

说的太笼统了,setTimeout的this指的是window,而箭头函数指的不是setTimeout的this是function的this,都是window对象,所以这个apply没有什么意思。

佼人 2022-05-04 13:57:51

用箭头函数的目的是为了让fn.apply的this和arguments都是闭包return的函数的this和arguments。
下面应该是常用场景的代码,可以参考这个代码思考实现

function debounce(fn) {
  let timeout = null; // 创建一个标记用来存放定时器的返回值
  return function () {
    clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
    timeout = setTimeout(() => {
      // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
      fn.apply(this, arguments);
    }, 500);
  };
}

class InputHandler extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: '' };
    this.handleChange = debounce(function (event) {
      this.setState({ value: event.target.value });
    });
  }

  render() {
    return <input type='text' onChange={this.handleChange} />;
  }
}
痞味浪人 2022-05-04 13:57:51

其实对于节流我有个疑问,最后一次回调是否应该必须触发。
当前的实现可能存在最后一次不触发的情况。

function throttle(fn) {
  let canRun = true; 
  return function () {
    if (!canRun) return; 
    canRun = false; 
    setTimeout(() => {
      fn.apply(this, arguments);
      canRun = true;
    }, 500);
  };
}

假设 time 是相对于第一次触发的时间差

time02004007009001000
canRun

七月上 2022-05-04 13:57:51

我优化了一下节流的逻辑,保证最后一次必须执行。

function throttle(fn, wait = 500) {
  let canRun = true; // 通过闭包保存一个标记
  let lastTimeId = null; // 纪录最后一次的timeoutId,每次触发回调时都是更新:变为null,或者更新timerId
  return function () {
    clearTimeout(lastTimeId);
    if (canRun) {
      canRun = false;
      setTimeout(() => {
        fn.apply(this, arguments);
        canRun = true;
      }, wait);
    } else {
      lastTimeId = setTimeout(() => {
        fn.apply(this, arguments);
      }, wait);
    }
  };
}
time02004007009001000
canRun

野侃 2022-05-04 13:57:50

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()

估计是改变this指向

薄情伤 2022-05-04 13:57:50

@zhongtingbing 你去试试在 不加 apply 时去 sayHi 函数里打印下 this看看什么

是指向window的。因为 sayHi 函数定义在全局中,所以调用时里面this指向window,
所以才需要加上 apply,显示绑定 this 值(input对象)到 sayH 函数里面去

这里用了apply确实使得this指向了input对象;对于“因为 sayHi 函数定义在全局中,所以调用时里面this指向window”,测试了一下直接使用fn(arguments)的话,在sayHi中打印this为undefined;js中this是在运行时绑定的,而不是定义时绑定的

弄潮 2022-05-04 13:57:50

有个问题,假如传入的方法是异步的,上述的节流方法是没用的啊,考虑把fn.apply(this, arguments)这一句放在setTimeout外面是不是会好一点?就像下面这样。

const myThrottle2 = function (func, wait = 50) {
  var canRun = true
  return function (...args) {
    if (!canRun) {
      return
    } else {
      canRun = false
      func.apply(this, args) // 将方法放在外面, 这样即便该函数是异步的,也可以保证在下一句之前执行
      setTimeout(function () {canRun = true}, wait)
    }
  }
}
允世 2022-05-04 13:57:50

@zhongtingbing 你去试试在 不加 apply 时去 sayHi 函数里打印下 this看看什么
是指向window的。因为 sayHi 函数定义在全局中,所以调用时里面this指向window,
所以才需要加上 apply,显示绑定 this 值(input对象)到 sayH 函数里面去

这里用了apply确实使得this指向了input对象;对于“因为 sayHi 函数定义在全局中,所以调用时里面this指向window”,测试了一下直接使用fn(arguments)的话,在sayHi中打印this为undefined;js中this是在运行时绑定的,而不是定义时绑定的

@Liubasara 是的,应该改为「因为 sayHi 函数是在全局中运行,所以this指向了window」,不过你说的「测试了一下直接使用fn(arguments)的话,在sayHi中打印this为undefined」是不对的哦,不显示绑定,是这里是指向window的。截图如下:

噩梦成真你也成魔 2022-05-04 13:57:50

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()

楼上大佬说的是对的,但是要注意这里的this(input)是addEventListener中调用回调的时候传进来的,这和是不是箭头函数没关系。
另外,因为不确定入参的数量,所以利用apply还可以传入扩展后的arguments(如果不兼容...arguments语法的话)。
已上。

鼻尖触碰 2022-05-04 13:57:50

@KouYidong 节流函数有点问题,第一次应该是立即执行,而不是delay 500ms后再执行

陌伤浅笑 2022-05-04 13:57:41

请问防抖那里可以写成
setTimeout(fn.bind(this), 500)
吗(小白的疑问)

慕巷i 2022-05-04 13:53:59

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()

@Carrie999 为了保证sayHi执行时的this指向input

ぃ双果 2022-05-04 13:44:53

@zhongtingbing 你去试试在 不加 apply 时去 sayHi 函数里打印下 this看看什么

是指向window的。因为 sayHi 函数是在全局中调用运行,所以 this 指向了 window,所以才需要加上 apply,显示绑定 this 值(input对象)到 sayH 函数里面去

烟柳画桥 2022-05-04 13:43:27

@zhongtingbing
加上 apply 确保 在 sayHi 函数里的 this 指向的是 input对象(不然就指向 window 了,不是我们想要的)。
这里的箭头函数依旧是指向 input 对象。

心欲静而疯不止 2022-05-04 13:38:21

@Carrie999 关键在第一个参数,为了确保上下文环境为当前的this,所以不能直接用fn。

请问为甚么你要确保fn执行的上下文是this?在这个箭头函数里this又是指向的谁?

千纸鹤 2022-05-04 12:54:25

@Carrie999 call 和 apply 可以了解一下

苍景流年 2022-05-03 06:39:26

@Carrie999 关键在第一个参数,为了确保上下文环境为当前的this,所以不能直接用fn。

清晨说ぺ晚安 2022-05-02 10:04:59

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()

~没有更多了~

关于作者

沉默的熊

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

已经忘了多久

文章 0 评论 0

15867725375

文章 0 评论 0

LonelySnow

文章 0 评论 0

走过海棠暮

文章 0 评论 0

轻许诺言

文章 0 评论 0

信馬由缰

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文