Advanced-Frontend/Daily-Interview-Question

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

yd160513 opened this issue · 127 comments

  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));

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

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

@Carrie999 call 和 apply 可以了解一下

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

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

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

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

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

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

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

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

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

估计是改变this指向

fan-2 commented

image
image
image

@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是在运行时绑定的,而不是定义时绑定的

有个问题,假如传入的方法是异步的,上述的节流方法是没用的啊,考虑把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)
    }
  }
}

@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的。截图如下:
image

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

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

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

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

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

注意this指向问题。

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

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

  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();
      }
    }
  }

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

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

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

@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. 解决办法

解决办法

xuxb commented

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

防抖添加个 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);
      }
    }
  }
}

防抖:

当一次事件发生后,事件处理器要等一定阈值的时间,如果这段时间过去后 再也没有 事件发生,就处理最后一次发生的事件。假设还差 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)
  }
}

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

防抖 :

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)
		}
	}
}

@Liubasara

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

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

函数防抖解决的是事件频繁触发的时候,怎么安排任务执行。只有触发操作后超过指定的间隔说明这一次触发才有效,否则就要重新计时等待。

/**
* @param {Function} fn 回调任务
* @param {Number} interval 触发间隔
* @returns {Function}
*/
function debounce(fn, interval = 500) {
     let timer = null;
     return (...args) => {
          clearTimer(timer);
          timer = setTimeout(() => {
               fn.apply(this, args);
          }, interval);
     }
}
// 调用例子
div.onscroll = debounce(cb, 1000)(cc=123, dd=234);

防抖

当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

应用场景:按钮点击事件/input事件,防止用户多次重复提交

手写debounce:

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  if (typeof func !== 'function') {
    throw new TypeError('Error')
  }
    
  // 缓存一个定时器id
  let timer = null;
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

节流

每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作

应用场景:

  • 鼠标/触摸屏的mouseover/touchmove事件
  • 页面窗口的resize事件
  • 滚动条的scroll事件

手写throttle:

function throttle(func, wait) {
    var previous = 0;
    return function(...args) {
        let now = Date.now();
        if (now - previous > wait) {
            func.apply(this, args);
            previous = now;
        }
    }
}

// 或者

function throttle(func, wait) {
    let timer = null;
    return function(...args) {
        if (!timer) {
            timer = setTimeout(() => {
                func.apply(this, args);
            	timer = null;
            }, wait)
        }
    }
}

防抖和节流可视化比较:http://demo.nimius.net/debounce_throttle/

区别:执行频率不同

防抖:

function debounce (fn, time) {
    var timeout
    return function (...args) {
        if (timeout) {
            clearTimeout(tiemout)
        }
        timeout = setTimeout(() => {
            fn.apply(this, args)
        }, time)
    }
}

节流:

function throttle (fn, time) {
    var timeout
    return function (...args) {
        if (!timeout) {
            timeout = null
            timeout = setTimeout(() => {
                timeout = null
                fn.apply(this, args)
            }, time)
        }
    }
}

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

  • 防抖
    在任务高频率触发的时候,只有触发间隔超过制定间隔的任务才会执行
  • 节流
    在制定间隔内任务只执行一次
  • 区别
    一定时间内任务执行的次数。
    比如一个事件每1s触发很多次,平均每10ms触发一次。
    节流,假设时间间隔为100ms,在100ms内只能执行一次事件回调函数,1s内函数的调用次数为:1000 / 100 = 10次
    防抖,假设时间间隔为100ms,时间触发的间隔必须要大于100ms,才调用回调函数。因为触发间隔=10ms < 100ms,1s内函数的调用次数为:0;
  • 节流实现
    function throttle(fn, interval) { let canRun = true; return function () { if (!canRun) { return; } canRun = false; setTimeout(() => { fn.apply(this, arguments); canRun = true; }, interval); } }
  • 防抖实现
    function deBounce(fn, wait) { let timer = null; return function () { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { fn.apply(this, arguments); }, wait); } }

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)
}
}

你这里的immediate有问题把?如果我传了第三个参数为true的话,岂不是fn会执行两次(if语句下面的timer也会执行)?

防抖:n秒内函数只会执行一次,如果n秒内事件再次被触发,则重新计算时间

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

节流:事件在n秒内只会执行一次,在n秒内事件不再触发

function throttle(fn, time){
    let isOk = true;
    return ()=>{
      if(!isOk) return;
      isOk = false;
      setTimeout(()=>{
         fn.apply(this, arguments);
         isOk = true;
      }, time);
   }
}
/**
 * Difference between debounce and throttle
 * In summary, 
 * throttle says, “Hey, we heard you the first time, but if you want to keep going, no problem. 
 * We’ll just ignore you for a while.” 
 * Debounce says, “Looks like you’re still busy. No problem, 
 * we’ll wait for you to finish!”
 */
/**
 * 快速书写一个防抖函数
 * @description 只要一直调用, callback 将不会被触发
 * 在一次调用结束后, 只有等待 timeout ms 时间, 才能继续调用 callback
 * immediate 决定触发时机
 * @example 
 * 1. 点击按钮发送请求(保存数据之类的)
 * 2. 搜索时自动联想
 * 3. 自动保存
 * 4. Debouncing a resize/scroll event handler
 */
function debounce(callback, timeout, immediate) {
  let timer;
  return function () {
    const context = this; // 持有执行上下文
    const args = arguments; // 记录传参
    const later = function () {
      timer = null; // 贤者时间过了,重振旗鼓,重置为初始状态
      if (!immediate) callback.apply(context, args); // 设置为尾部调用才延时触发
    }
    const callNow = immediate && !timer; // 如果确认允许首部调用,且首次调用,那么本次立即触发
    clearTimeout(timer); // 杀掉上次的计时器,重新计时
    timer = setTimeout(later, timeout); // 重启一个计时器,过了贤者时间之后才触发
    callNow && callback.apply(context, args); // 设置为首部调用立即触发
  }
}

/**
 *  快速书写一个节流函数
 * @description 一直调用 callback, 每隔 timeout ms 时间 callback 触发一次
 * 在 timeout ms 时间内的调用将不触发
 * @example
 * 1. Throttling a button click so we can’t spam click 控制疯狂按钮的响应频率
 * 2. Throttling an API call 控制 API 的调用频率
 * 3. Throttling a mousemove/touchmove event handler 控制频繁触发事件的相应频率
 */
// solution1 记录时间比较
function throttle(callback, timeout) {
  let triggerTime; // 记录每次真正触发时间
  return function () {
    const context = this; // 持有执行上下文
    const args = arguments; // 记录传参
    if (triggerTime === undefined // 首次调用
      || Date.now() - triggerTime > timeout) { // 贤者时间已经过去
      triggerTime = Date.now(); // 记录真正触发时间
      callback.apply(context, args); // 可以触发回调
    }
  }
}
// solution2 间隔时间反转标志位
function throttle(callback, timeout) {
  let disable; // 触发回调是否禁用
  return function () {
    const context = this; // 持有执行上下文
    const args = arguments; // 记录传参
    if (!disable) { // 首次调用或者贤者时间过了,禁用解除
      callback.apply(context, args); // 可以触发回调
      disable = true; // 马上禁用
      setTimeout(_ => disable = false, timeout); // 贤者时间过了,禁用解除
    }
  }
}

函数节流

概念

在 n 秒中只执行一次

实现原理

通过 setTimeout 执行

代码实现

// 初步实现
const throttle = function (fn, time) {
  let canRun = true
  // 闭包
  return () => {
    if (canRun) {
      canRun = false
      setTimeout(() => {
        canRun = true
        fn()
      }, time)
    }
  }
}

function sayThrottle() {
  console.log('我是节流,我在固定的时间内执行一次')
}
window.onscroll = throttle(sayThrottle, 1000)

函数防抖

概念

在 n 秒内重新触发,会重新开始计算时间

实现原理

通过 setTimeout 和 clearTimeout 实现

代码实现

function debounce (fn, time) {
  let timer = null
  // 闭包
  return () => {
    // 每次都会重新开始计算时间
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn()
    }, time)
  }
}

function sayDebounce() {
  console.log('我是防抖,每次触发我都会重新计算一次时间')
}

btn.onclick = debounce(sayDebounce, 1000);

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

他这个已经用了箭头函数了,直接fn.apply();也是可以的吧

MoxTY commented

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()
我觉得你应该了解一下什么是apply和call

// 防抖
const debounce = (fun, wait) => {
    let handle;
    return (...args) => {
        clearTimeout(handle);
        handle = setTimeout(() => fun(...args), wait);
    }
}

// 节流
const throttle = (fun, wait) => {
    let isRuning = false;
    return (...args) => {
        if (!isRuning) {
            isRuning = true;
            setTimeout(() => isRuning = false, wait)
            fun(...args);
        }
    }
}

// eg.
const fun1 = debounce(f1 => console.log(f1), 300);
const fun2 = throttle(f2 => console.log(f2), 300);

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

@Carrie999 渐渐的改变已有的认知,完整的函数调用应该是前者:

函数名.apply(当前上下文, [ 参数1, 参数2, .... ])

后者可以理解成是方便撰写的"糖", 改变过来后,对this指向的问题的理解也有很大的帮助。

节流函数

// 基础版本: 基于时间实现节流函数
function throttle(fx, delay = 300) {
    let pre = Date.now();
    return (...args) => {
      let cur = Date.now();
      console.log(cur - pre)
      if(cur - pre >= delay) {
        fx.apply(this, args);
        pre = Date.now();
      }
    }
  }

// 基础版本: 基于定时器实现节流函数
function throttle(fx, delay = 300) {
    let time = null;
    return (...args) => {
      if(!time) {
        time = setTimeout(() => {
          fx.apply(this, args);
          time = null;
        }, delay);
      }
    }
  }

// 基于定时器+时间实现节流函数, 第一次触发, 后面每delay周期执行一次, 最后一次触发执行
  function throttle(fx, delay = 300) {
    let pre, time;
    return (...args) => {
      const now = Date.now();
      if (pre && now < delay + pre) {
        // 处于delay期间内
        clearTimeout(time);
        time = setTimeout(() => {
          pre = now;
          fx.apply(this, args);
          console.log('最后一次执行');
        }, delay);
      } else {
        pre = now;
        // 第一次执行
         // delay周期执行一次
        fx.apply(this, args);
      }
    };
  }

防抖函数

function debounce(fx, delay = 300) {
    let time = null;
    return (...args) => {
      if (time) {
        clearTimeout(time);
      }
      time = setTimeout(() => {
        fx.apply(this, args);
      }, delay);
    };
  }

可以看这里, 讲解的比较细, 也可以直接看lodash的源码
https://yuchengkai.cn/docs/frontend/#%E9%98%B2%E6%8A%96

函数节流

概念

在 n 秒中只执行一次

实现原理

通过 setTimeout 执行

代码实现

// 初步实现
const throttle = function (fn, time) {
  let canRun = true
  // 闭包
  return () => {
    if (canRun) {
      canRun = false
      setTimeout(() => {
        canRun = true
        fn()
      }, time)
    }
  }
}

function sayThrottle() {
  console.log('我是节流,我在固定的时间内执行一次')
}
window.onscroll = throttle(sayThrottle, 1000)

函数防抖

概念

在 n 秒内重新触发,会重新开始计算时间

实现原理

通过 setTimeout 和 clearTimeout 实现

代码实现

function debounce (fn, time) {
  let timer = null
  // 闭包
  return () => {
    // 每次都会重新开始计算时间
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn()
    }, time)
  }
}

function sayDebounce() {
  console.log('我是防抖,每次触发我都会重新计算一次时间')
}

btn.onclick = debounce(sayDebounce, 1000);

我想给sayDebounce方法传递额外的参数,再调用的时候应该怎么写?

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

因为调用的时候要接收传入的参数,arguments就是用于接收上一级的参数的this相当于占位符,在这里可以使用Null来代替

wzpxx commented

防抖:上个定时器还没执行完,就清除新产生的定时器。
节流:规定时间内,只产生和执行一个定时器。

防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

也贡献一个ts版本

/**
 * [throttle 时间戳+定时器 节流函数]
 * @param  {[type]} func:   any           [description]
 * @param  {[type]} delay?: number        [毫秒时间]
 * @return {[type]}         [description]
 */
export function throttle(this: any, func: any, delay = 0): any {
    let timer
    let startTime = Date.now()
    return args => {
        let remainTime = delay - (Date.now() - startTime)
        if (remainTime <= 0) {
            func.apply(this, args)
            startTime = Date.now()
        } else {
            timer && clearTimeout(timer)
            timer = setTimeout(func, remainTime)
        }
    }
}

例子中的函数调用写在了箭头函数中,箭头函数中的this在函数定义时就决定了,所以使用call或者apply时的this就是最后return出来的函数中的this , return出来的匿名函数为普通函数它的this由调用者决定,所以最后this会指向最终的调用者input

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

不能,那样写虽然看起来更简洁,但是arguments传不进去了

/**
 * 节流(被调用n次,只在time间隔时间点运行)
 * @param fn 回调函数
 * @param time 延时
 */
function throttle (fn, time = 300) {
  if (fn.pending) {
    return;
  }

  fn();
  fn.pending = true;

  setTimeout(() => {
    fn.pending = false;
  }, time);
}

/**
 * 防抖(被调用n次,只运行一次)
 * @param fn 回调函数
 * @param time 延时
 */
function debounce (fn, time = 300) {
  clearTimeout(fn.tid);
  fn.tid = setTimeout(fn, time);
}

我的写法

防抖是不是就是高频事件的回调函数的执行时间,确保回调函数n秒之内只执行一次。节流是不是就是高频事件在n秒内只执行一次。控制高频事件的执行次数。--个人愚见

joyz0 commented
function debounce(callback, timeout, immediate) {
  let timer;
  return function () {
    const context = this; // 持有执行上下文
    const args = arguments; // 记录传参
    const later = function () {
      timer = null; // 贤者时间过了,重振旗鼓,重置为初始状态
      if (!immediate) callback.apply(context, args); // 设置为尾部调用才延时触发
    }
    const callNow = immediate && !timer; // 如果确认允许首部调用,且首次调用,那么本次立即触发
    clearTimeout(timer); // 杀掉上次的计时器,重新计时
    timer = setTimeout(later, timeout); // 重启一个计时器,过了贤者时间之后才触发
    callNow && callback.apply(context, args); // 设置为首部调用立即触发
  }
}

请问immediate=true时的防抖和节流有什么区别 @LiuMengzhou

哪有那么复杂?
简而言之:
节流:高频触发的事件,在指定的单位时间内,只响应第一次
防抖:高频触发的事件,在指定的单位时间内,只响应最后一次,如果在指定时间再次触发,则重新计算时间.

节流的栗子:

 function throttle (fn, delay) {
   let lastTime = 0;  // 上次触发的时间
   return function () {
     let now = Date.now();
     if (now - lastTime > delay) {
       fn.call(this);
       lastTime = now; // update时间
     }
   }
 }


// 调用
 
 document.onscroll = throttle(function () {
   console.log('- throttle - ')
 }, 1000)
 
 防抖的栗子:
 // 定义
 function debounce (fn, delay) {
   let timer = null ; // 上次的计数器
   return function () {
     clearTimeout(timer)
     timer = setTimeout(function () {
       fn.call(this)
     }, delay)
   }
 }
 
 // 调用
 document.querySelector('.butn').onclick = debounce(function () {
   console.log('- debounce -')
 }, 1000)

给你们一个可以装饰class内 箭头函数的 debounce 封装

example:

class A {
  @debounce_next({delay: 400})
  scrolling = ()=>{
    console.log('hahah')
  }
}
/**
 * @description 防抖动:(decorator)可装饰类内箭头函数
 * @param {object} params - 配置
 * @param {number} params.delay - 时间阀值(单位:ms),默认:delay=300
 * @param {bool} params.immediate - 初始是否立刻执行,默认:immediate=false
 * @returns {function} - 返回装饰器方法
 */
export const debounce_next = (params = {}) => {
  // reference:http://es6.ruanyifeng.com/#docs/decorator#%E6%96%B9%E6%B3%95%E7%9A%84%E4%BF%AE%E9%A5%B0
  return function(target, name, descriptor) {
    let timer = null;
    const { delay = 300, immediate = false } = params;

    // high order function
    if (!descriptor || (arguments.length === 1 && typeof target === 'function')) {
      return createDebounce(target);
    }

    function createDebounce(fn) {
      return function debounce() {
        if (immediate && !timer) {
          fn.apply(this, arguments);
        }
        if (timer) {
          clearTimeout(timer);
        }

        let argumentsCopy = arguments;
        let that = this;

        timer = setTimeout(function() {
          if (!immediate) {
            fn.apply(that, argumentsCopy);
          }
          timer = null;
        }, delay);
      };
    }

    // 修饰类内的箭头函数
    if (descriptor.initializer) {
      return {
        enumerable: false,
        configurable: true,
        get: function() {
          return createDebounce(descriptor.initializer.call(this));
        },
      };
    }

    return descriptor;
  };
};

TS 版源码:any

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

因为要绑定fn的this指向,在setTimeout里面执行的函数都会在全局作用域执行所以this会指向window,但是fn执行时this指向的上下文环境应该是当前执行的函数,所以要用apply来绑定fn的this指定到当前

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

你argument怎么解决?

基于事件的,感觉更优雅。

const debounce = (eventTarget, eventName, wait) => {
    let handle;
    let event = new Event(eventName);
    return (detail) => {
        event.detail = detail;
        clearTimeout(handle);
        handle = setTimeout(() => eventTarget.dispatchEvent(event), wait);
    }
}

const throttle = (eventTarget, eventName, wait) => {
    let isRuning = false;
    let event = new Event(eventName);
    return (detail) => {
        if(isRuning) return;
        isRuning = true;
        setTimeout(() => isRuning = false, wait);
        event.detail = detail;
        eventTarget.dispatchEvent(event);
    }
}

let target = window;

target.addEventListener('resize', debounce(target, 'myonresizedebounce', 3000))

target.addEventListener('myonresizedebounce', (event) => {
    console.log({name: 'myonresizedebounce', event});
})

target.addEventListener('resize', throttle(target, 'myonresizethrottle', 1000))

target.addEventListener('myonresizethrottle', (event) => {
    console.log({name: 'myonresizethrottle', event});
})

不能简单掌握基础的就够了。面试要有亮点。不然基础的知识大家都回答了,面试官只能找有特色的了。

可以看看这个
别只掌握基础的防抖和节流了

防抖:
一定时间内只执行一次,若这段时间内发起新的动作,那么时间从头开始计时。比如时间设置为5s,只要动作间隔时间小于5s,那么这个动作不会发生,会等待最后一次动作之后5s触发。

    const debounce = (fn, dur = 500) => {
      let timer = null
      return function(...args) {
        clearTimeout(timer)
        timer = setTimeout(()=>{
          fn.apply(this, args)
        }, timer)
      }
    }

节流:
一定时间内只执行一次,若这段时间内发起新的动作,无视之。每隔多少秒执行一次。

  const throttle = (fn, dur = 5000) => {
    let timer = null
    return function(...args) {
      if (timer == null) {
         fn.apply(this, args)   // 放在这里是触发动作后马上执行,5s后执行下一次动作。
         timer = setTimeout(()=> {
            // fn.apply(this, args)   // 放在这里是触发动作5s后执行,然后5s后执行下一次动作。
            timer = null
        })
      }
    }
  }

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

this

为了 给input 绑定 事件

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

上面的return function () { 这样写的目的到底是为了闭包保存timeout 如果是为了保存timeout 那为申明要手动销毁

防抖

原理:在每次函数执行之前先清空上一次设置的定时器,原因是:如果执行间隔大于n秒,那么先前的定时任务一定已经执行完毕,当执行clearTimeout时其实并没有定时器可清除;否则定时器就会被清除,然后重新计时

function debounce(fn, wait, immediate) {
    var timer = null;
    return function() {
        var context = this;
        var args = arguments;

        // 判断首次是否需要立即执行
        if (immediate) {
            fn.call(context, ...args);
            immediate = false;
        }

        // 清除定时器
        clearTimeout(timer);
        timer = setTimeout(function() {
            fn.call(context, ...args);
        }, wait);
    };
}

节流

原理:在每次函数执行之前先判断是否存在定时器,存在则跳过本次执行,否则设置新的定时器

function throttle(fn, wait, immediate) {
    var timer = null;
    return function() {
        var context = this;
        var args = arguments;

        // 判断首次是否需要立即执行
        if (immediate) {
            fn.call(context, ...args);
            immediate = false;
        }

        // 如果当前存在定时器,返回;否则设置定时器
        if (timer) return;

        timer = setTimeout(function() {
            fn.call(context, ...args);
            // 函数执行完毕后,清除定时器
            clearTimeout(timer);
            timer = null;
        }, wait);
    };
}

问个小白的问题,为什么要return一个函数出来,不然就会直接执行出第一次的结果,就再也触发不了了

使用 timer 或 Date 都可以实现
写了个demo
防抖和节流实现

我工作中主要的应用场景,
1、防抖:在搜索输入框中,根据用户输入内容变化动态展示联想搜索词
2、节流:抢票啊、提交数据、切换、轮播和部分Animate动画中,执行完上一次操作后才能再次点击执行对应的函数

// 防抖是在指定时间内再次触发只会响应一次(最后那次)表现为延迟调用
const debounceIfy = (fn, wait) => {
  let timeoutId;
  return (...args) => {
    if (timeoutId) {
      // timeoutId 如果存在说明上一个fn任务还在等待,应该重制它为最近一次调用
      clearTimeout(timeoutId);
    }
    timeoutId = setTimeout(() => {
      fn(...args);
    }, wait);
  };
};

const TestDebounceIfy = () => {
  console.log('====TestDebounceIfy====');
  const fn = count => {
    console.log(count);
  };
  const wait = 500;
  const debounceFn = debounceIfy(fn, wait);

  debounceFn(1);
  debounceFn(2);
  debounceFn(3);
  setTimeout(() => {
    debounceFn(4);
  }, wait * 0.4);
  setTimeout(() => {
    debounceFn(5);
  }, wait * 1);
  setTimeout(() => {
    debounceFn(6);
  }, wait * 1.2);

  setTimeout(() => {
    debounceFn(7);
  }, wait * 2.4); // 只有距上一次间隔大于1个wait(2.4-1.2>1)才会被调用
};

// TestDebounceIfy();
// 6
// 7

//节流是指让事件的触发按指定速率触发,表现为立即调用但控制速率
const throttleIfy = (fn, time) => {
  let canRun = true;
  return (...args) => {
    if (canRun) {
      canRun = false;
      fn(...args);
      setTimeout(() => {
        canRun = true; // time 过后对 fn 放行
      }, time);
    }
  };
};

const TestThrottleIfy = () => {
  console.log('====TestThrottleIfy====');
  const fn = count => {
    console.log(count);
  };
  const time = 500;
  const throttleFn = throttleIfy(fn, time);

  throttleFn(1);
  throttleFn(2);
  throttleFn(3);
  setTimeout(() => {
    throttleFn(4);
  }, time * 0.4);
  setTimeout(() => {
    throttleFn(5);
  }, time * 1);
  setTimeout(() => {
    throttleFn(6);
  }, time * 1.2);

  setTimeout(() => {
    throttleFn(7);
  }, time * 2.4);
};

// TestThrottleIfy(); // 每过一个 time 放行一次
// 1
// 5
// 7

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

可以this 指向 以及当前的数据也都传递给下面都方法

请问,为什么要 fn.apply(this, arguments);而不是这样 fn()
考虑到还有参数的情况下,改变this指向

节流

n秒内只会执行一次,节流会稀释函数的执行频率

/**
 * @name throttle, 节流函数,(高频操作,性能优化)
 * @param {*} func
 * @param {number} [wait = 300]
 * @returns
 * @example
 * 
 *  异步使用
 const throttleAjax = throttle((newParams)=>{
  return new Promise((resolve, reject) => {
    xhr({
      url: '/api',
      type: 'POST',
      data: newParams,
      success: res => {
        const {data} = res;
        const arr = isArray(data) ? data : [];
        resolve(arr);
      },
      error: err => {
        reject(err);
      }
    });
  });
}, 300);
export function apiExample (params) {
  const newParams = filterParams(params);
  return new Promise((resolve) => {
    const keys = Object.keys(newParams);
    if (!keys.length) {
      resolve([]);
    } else {
      throttleAjax(newParams).then(res => {
        resolve(res);
      });
    }
  });
}
 */

function throttle (func, wait = 300) {
  var timeout;
  return function () {
    var context = this;
    var args = arguments;
    return new Promise((resolve) => {
      if (!timeout) {
        timeout = setTimeout(() => {
          timeout = null;
          resolve(func.apply(context, args));
        }, wait);
      }
    });
  };
}

export default throttle;

防抖

每次触发事件时都取消之前的延时调用方法

/**
 * @name debounce, 防抖函数,(高频操作,性能优化)
 * @param {*} fn
 * @param {number} [step=100]
 * @returns
 * @example
 * /
function debounce(fn, step = 100) {
  let timeout = null;
  return function () {
    clearTimeout(timeout);
    return new Promise(resolve => {
      timeout = setTimeout(() => {
        resolve(fn.apply(this, arguments));
      }, step);
    });
  };
}

export default debounce;

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

fn.apply(this,arguments); 当前是this;fn()的this是window

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

fn.apply(this,arguments) 这里的this指的也是window吧?fn()的this也是window呀?有什么区别吗?

debounce

防抖,是指一个事件触发后在单位时间内,如果发生重复触发了同一事件,则取消上一次的事件,并重新计时⌛️

(后面触发的事件执行,替代了前面的事件)

throttle

节流,是指在单位时间内, 只会触发一次事件,如果事件触发后,又重复触发了同一事件,则忽略后面触发的事件,直到第一次事件的计时⌛️结束

(前面触发的执行前,忽略后面的事件)

有个问题,假如传入的方法是异步的,上述的节流方法是没用的啊,考虑把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)
    }
  }
}

解释下呢,func异步不异步有啥影响?
不过你这种写法倒是有个优点,就是函数的执行时机与其调用时机基本吻合,不会延迟执行

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

@Liubasara 的回复就没有这个问题,把函数的执行放到timeout之外去,timeout单纯做计时作用就行

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

不可以。必须得清除定时器,不然,就不是防抖,而是把抖动滞后了。

请问大佬们,如果问节流和防抖的区别,我可不可以回单说两者本质上都是稀释了事件的执行频率,但是防抖是通过取消上一次事件的执行而稀释,而节流是阻止下次事件的执行而稀释?

下图能很清楚的演示在 mousemove 情况下

防抖(debounce)和节流(throttle)表现上的差异。

16f6eba8e2d1fc04

这个arguments怎么传进去的,我怎么看都是undefined @KouYidong

看了几个同学都在说this指向问题,感觉没有说到点上。上面两个函数基本上都是对于高价函数的应用,熟悉函数式编程的同学可能比较容易懂。它们都是通过传入一个函数 、返回一个函数来提高函数的灵活性。这里面的fn.apply(this, arguments)有同学问为什么不直接写fn()。有些同学说是因为this的原因,个人感觉这里虽然说apply、call 这种魔法糖确实可以改变执行时候this的指向,但是对于节流、防抖这种工具函数来说 设计的时候恰恰是不希望改变原始fn的this指向的。 试想一下如果是你在用这个工具函数包裹一个自己定义的函数这时候里面的this被改变了这样就会让你很困惑。所以这里的this其实还是和fn被调用的行为保持一致的。 从内部分析也能得到相同的结论, 因为像setTimeout这种函数里面使用箭头函数this是会指向外层函数的也就是父作用域的this,也就是返回的那个函数。就像使用自己的入参函数一样。其实不直接使用fn()主要是因为fn不能很好支持多参数扩展 而直接使用fn.apply(this, argument)是可以支持任意参数 也符合工具函数的设计目的。当然demo里的直接写fn()应该也是没问题的只要不涉及传参。

y1324 commented

if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
console.log('700毫秒后输出');
}, 250);

请问我这种有什么优点和弊端?

问个小白的问题,为什么要return一个函数出来,不然就会直接执行出第一次的结果,就再也触发不了了

每次调用函数的变量都是新的,最多就是加个setTimeout 把抖动延后了而已,并不能起作用。

const debounce = (fn, wait = 100) => { let timer = null; return function(...args){ if(timer !== null) clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); } }

const throttle = (fn, delay = 500) => { let timer = null; return (...args) => { if (!timer) return; timer = setTimeout(() => { fn(...args); clearTimeout(timer); timer = null; }, delay); }; };

防抖

使函数在规定的时间范围内只调用一次,如果规定时间未到,则重新计时。例如用户频繁点击,只需要响应一次即可

// 思路: 利用计时器,如果计时器时间未到时调用函数则重置计时器
function debouce(fn,wait) {
  let timer = null
  return function(...args) {
      if(timer) {
        clearTimeout(timer)
      }
      timer = setTimeout(()=>{
          fn.apply(this,args)
       },wait)
  }
}

节流

用来降低函数的调用频率, 例如监听窗口resize和scroll事件

// 思路: 记录函数每次调用的毫秒数,如果上次调用的时间和现在调用的时间查超过规定时间,则调用该函数
function throttle(fn,wait) {
   let previous = Date.now()
   return function(...args) {
      let now = Date.now()
      if(now - previous >= wait) {
            fn.apply(this,args)
            previous = now
      }
   }
}

区别

  1. 防抖 - 在规定时间内只会调用一次
  2. 节流 - 函数会调用多次,但是调用频率会降低

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

因为要保证执行fn函数的上下文和debounce函数的上下文保持一致。事件绑定回调函数中,this指向的是事件句柄对象

问个小白的问题,为什么要return一个函数出来,不然就会直接执行出第一次的结果,就再也触发不了了

return出来一个函数形成了一个闭包,保证了控制防抖的timeout变量和节流的canRun变量在全局中的唯一性,不会每次执行函数就生成另一个独立的timeoutcanRun

你这个疑问的知识点主要是闭包,我推荐一篇比较通俗的回答JS 中的闭包是什么?

防抖debounce:高频触发的事件,在指定的单位时间内,只响应最后一次,如果在指定时间再次触发,则重新计算时间。

function debounce(fn,delay){
   var timer;
   return function(){
     clearTimeout(timer);
     timer = setTimeout(()=>{
        fn.apply(this,arguments); //保证执行fn函数的上下文和debounce函数的上下文保持一致
       },delay)
   }
}
  • 再详细解释fn.apply
  • 因为return 返回的函数是一个匿名函数,在匿名函数里直接执行fn()的话,就相当于在window下执行函数,this是指向window的
  • 所以为了让fn的this指向原来的地方,要用到apply
  • 换句话说 只要是用 return function() { //... } 这种写法的高阶函数 都需要用call等方法将正确的this指回去

节流throttle:高频触发的事件,在指定的单位时间内,只响应第一次;

function throttle(fn,delay){
  var nowTime = +new Date( );
  return function(){
    if(+new Date( ) - nowTime > delay){ //当前时间减去初始时间大于延时才会进行响应
        fn.apply(this,arguments);
        nowTime = +new Date( );
    }
  }
}

收获挺多

防抖

// immediate代表是否先执行
function _debounce(fn, immediate = false) {
        let timer = null;
        return () => {
            if (immediate && !timer) {
                fn.apply(this, arguments);
            }
            if (timer) clearTimeout(timer);
            timer = setTimeout(() => {
                if (immediate) {
                    timer = null;
                } else {
                    fn.apply(this, arguments);
                }
            }, 500);
        }
    }

节流

function _throttle(fn, immediate = false) {
        let timer = null;
        return () => {
            if (!timer) {
                if (immediate) {
                    fn.apply(this, arguments);
                }
                timer = setTimeout(() => {
                    timer = null;
                    console.log('init');
                    if (!immediate) {
                        fn.apply(this, arguments);
                    }
                }, 500)
            }
        }
    }

demo

function sayHi() {
        console.log('防抖成功');
    }
   var inp = document.getElementById('inp');
    inp.addEventListener('click', _throttle(sayHi, true));

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

首先 我们想要达到的效果是: fn 被执行时候的 this 指向是在被debouce包裹之前那个对的this指向,在以下代码中就是input对象

input.addEventListener('input', sayHi)  // 不用debounce this应该指向input对象

而debunce函数返回的是一个函数,这个函数是会被正确的调用的,所以这里的this指向就是正确的this,所以把这个this绑定到fn上,是没问题的;
那么为什么fn直接调用会指向window呢?因为return 的是一个匿名函数,而在匿名函数里执行函数,跟全局环境执行差不多,函数的this指向一般都是window。

function debounce() {
  return function() {
    fn()  // 这样直接调用和在window全局环境调用差不多 this永远指向window 
  }
}

换句话说 只要是用 return function() { //... } 这种写法的高阶函数 都用用call等方法将正确的this指回去

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

fn.apply(this,arguments) 这里的this指的也是window吧?fn()的this也是window呀?有什么区别吗?

fn() 在匿名函数直接调用 和全局调用差不多 this都指向window 可以查看js高程 7.2.2 闭包

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

fn.apply(this,arguments) 这里的this指的也是window吧?fn()的this也是window呀?有什么区别吗?

fn() 在匿名函数直接调用 和全局调用差不多 this都指向window 可以查看js高程 7.2.2 闭包

首先,我真的有点没看懂具体的意思,大致认为你是不明白this的指向,先不提这个如果您能看到这个回复,还请说详细点...(也有可能是我理解问题,对不起...)但是,我自己排查了下,这个demo的确有个问题,就是你说的apply的this指向问题,我这用的箭头函数,所以,我的this指向一直是window,毕竟箭头函数不会更改this的指向,一直指向调用处的父类.所以我更改了下,你也可以尝试下对比下,apply和单独调用的打印,若有其他疑问,欢迎继续一起讨论~~~谢谢

function _debounce(fn, immediate = false) {
        console.log('_debounce', this);
        let timer = null;
        return function() {
            console.log('_debounce inner', this);
            if (immediate && !timer) {
                fn.apply(this, arguments);
            }
            if (timer) clearTimeout(timer);
            timer = setTimeout(() => {
                if (immediate) {
                    timer = null;
                } else {
                    fn.apply(this, arguments);
                    // fn();
                }
            }, 500);
        }
    }

    function sayHi() {
        console.log(this);
        console.log('防抖成功');
    }
    var inp = document.getElementById('inp');
    inp.addEventListener('click', _debounce(sayHi)); // 防抖

@KouYidong
canRun,我觉得这个标志其实可以换成定时器ID来控制,这样的话,也可以清除掉每次的定时器,除此之外,还需要立即执行一次。

function throttle(fn, wait) {
      let timer= null; // 保存定时器ID
      let immediate = true;
      return function () {
         if (!fn || typeof fn !== 'function') return;

         // 保证第一次执行不用等待
         if (immediate) {
           fn.apply(this, arguments);
           immediate = false;
           return;
         }
        if (timer) return; // 定时器存在,则说明还没执行需要return
        timer = setTimeout(() => { 
          fn.apply(this, arguments);
          clearTimeout(timer);
          timer = null;
        }, wait);
    };
 }

有个问题,假如传入的方法是异步的,上述的节流方法是没用的啊,考虑把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)
    }
  }
}

除了调用的时间点不一样,回调方法放在setTimeout里面和外面的效果是一样的吧, 跟是否异步无关吧?

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

不这样做拿不到传入的参数

// 防抖
// 触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间

// 思路:
// 每次触发事件时都取消之前的延时调用方法


function debounce(fn, wait = 1000) {
  let timer = null;
  return (...args) => {
    if (timer)clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, wait);
  };
}

// Test Case 模拟输入搜索
function search(text) {
  console.log(new Date(), text);
}

const handle = debounce(search, 1500);

let str = '';
for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    str += 'S';
    handle(str);
  }, Math.random() * 10000);
}


// 节流
// 高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率

// 思路:
// 每次触发事件时都判断当前是否有等待执行的延时函数

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

// Test Case 模拟页面滚动
function getPageHeight(h) {
  console.log(new Date(), h);
}

const test = throttle(getPageHeight, 1000);

let pageHeight = 0;
for (let i = 0; i < 1000; i++) {
  setTimeout(() => {
    pageHeight += 1;
    test(pageHeight);
  }, Math.random() * 10000);
}

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

这里用了箭头函数, 不需要重新绑定了吧? 箭头函数的this在编译的时候就定好了呀

@MiaLeung01 throttle不能用箭头函数,会使fnc.apply(this), this 的指向指到父函数。
比如: function test(p1) {console.log(p1)}, throttle(test, 200)('test'); log函数的p1没有使用test。

tjwyz commented
function debounce (fn, time) {
	let handle;
	return (...arg) => {
		if (handle) {
			clearTimeout(handle);
		}
		handle = setTimeout(()=>{
			fn(...arg);
		}, time)
	}
}


function throttle(fn, time) {
	let timeStamp = 0;
	return (...arg) => {
		if (Date.now() - timeStamp > time) {
			fn(...arg);
			timeStamp = Date.now();
		}
	}
}

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

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

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

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