Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yd160513 opened this issue Jan 23, 2019 · 127 comments
Open
Labels

Comments

@yd160513
Copy link

  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));
@yygmind yygmind changed the title 节流和防抖的个人见解 第三题:节流和防抖的个人见解 Feb 12, 2019
@Carrie999
Copy link

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

@barbara012
Copy link

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

@MinosIE
Copy link

MinosIE commented Feb 18, 2019

@Carrie999 call 和 apply 可以了解一下

@yangtao2o
Copy link

之前有看过一步步实现的文章,如下:

@zhongtingbing
Copy link

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

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

@lilywang711
Copy link

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

@lilywang711
Copy link

lilywang711 commented Feb 18, 2019

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

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

@yishuihan-001
Copy link

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

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

@Crazy404
Copy link

Crazy404 commented Feb 19, 2019

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

@Mathround
Copy link

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

估计是改变this指向

@fan-2
Copy link

fan-2 commented Feb 20, 2019

image
image
image

@Yolo-0317
Copy link

@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
Copy link

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

@lilywang711
Copy link

lilywang711 commented Feb 22, 2019

@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

@KiraYo4kage
Copy link

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

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

@mochen666
Copy link

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

@XMoonLights
Copy link

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

@sunnyxuebuhui
Copy link

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

注意this指向问题。

@roberthuang123
Copy link

roberthuang123 commented Mar 7, 2019

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

@MiaLeung01
Copy link

MiaLeung01 commented Mar 13, 2019

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

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

@thinkfish
Copy link

thinkfish commented Mar 14, 2019

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

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

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

@zengjiepro
Copy link

@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
Copy link

xuxb commented Mar 26, 2019

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

@XDfield
Copy link

XDfield commented Mar 29, 2019

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

@Jingce-lu
Copy link

Jingce-lu commented Mar 30, 2019

防抖:

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

@asd8855
Copy link

asd8855 commented Apr 12, 2019

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

@hytStart
Copy link

hytStart commented Apr 12, 2019

防抖 :

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

@gzwgq222
Copy link

gzwgq222 commented Apr 14, 2019

@Liubasara

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

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

@m7yue
Copy link

m7yue commented Oct 14, 2020

// 防抖:短时间内大量触发同一事件,只会执行一次函数
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(); // 更新初始时间
      }
  }
}

@wenheqi
Copy link

wenheqi commented Oct 20, 2020

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

@moxiaodegu
Copy link

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

为了给fn传参

@JaykeyGuo
Copy link

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

为了给fn传参

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

@mjj7890
Copy link

mjj7890 commented Oct 29, 2020

@FatDoge
Copy link

FatDoge commented Nov 10, 2020

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

@boluo-muma
Copy link

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

箭头函数白用了

@bread-kun
Copy link

immediate

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

@ruofee
Copy link

ruofee commented Dec 23, 2020

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

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

@BarneyRoos
Copy link

BarneyRoos commented Jan 6, 2021

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

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

@decSunshineHe
Copy link

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

@sayll
Copy link

sayll commented Mar 8, 2021

// 防抖

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

@hzq-up
Copy link

hzq-up commented Mar 19, 2021

  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秒后再执行回调吧?

@hzq-up
Copy link

hzq-up commented Mar 26, 2021 via email

@hzq-up
Copy link

hzq-up commented Mar 26, 2021 via email

@FansOne
Copy link

FansOne commented Mar 26, 2021 via email

@zhuzhile
Copy link

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

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

@Yrobot
Copy link

Yrobot commented Apr 21, 2021

用箭头函数的目的是为了让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} />;
  }
}

@Yrobot
Copy link

Yrobot commented Apr 21, 2021

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

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

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

time 0 200 400 700 900 1000
canRun true false false true false false

理想情况(eventloop不拥挤)fn执行时间节点为:500(0)、1200(700)
最后一次1000的没有触发

@Yrobot
Copy link

Yrobot commented Apr 21, 2021

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

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);
    }
  };
}
time 0 200 400 700 900 1000
canRun true false false true false false
lastTimeId null [200] [400] null [900] [1000]

理想情况(eventloop 不拥挤)fn 执行时间节点为:500(0)、1200(700)、1500[1000]

@lzxjack
Copy link

lzxjack commented Jun 18, 2021

😃😃😃 本人能力有限,欢迎大佬指正改进~

1. 手写防抖函数(debounce)

防抖函数功能:

在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时

比如一个搜索框,应用防抖函数后,当用户不断输入内容时,不会发送请求。只有当用户一段时间T内不输入内容了,才会发送一次请求。如果小于这段时间T继续输入内容的话,就会重新计算时间T,也不会发送请求。这样降低了发送请求的次数,提高性能的同时也提升了用户体验。

实现防抖函数:

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

实现效果:

上方输入框,下方显示区,不断输入内容时,下方显示区不会更新。只有在1s内不输入内容了,下方显示区才会更新内容。

2. 手写节流函数(throttle)

节流函数功能:

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

实现节流函数:

// func是用户传入需要防抖的函数
// wait是等待时间,若未传参,默认50ms
const throttle = (func, wait = 50) => {
    // 上一次执行该函数的时间
    let lastTime = 0;
    // 返回的函数是每次用户实际调用的节流函数
    return (...args) => {
        // 获取当前时间,并转化为number
        let now = +new Date();
        // 将当前时间和上一次执行函数时间对比
        // 如果差值大于设置的等待时间就执行函数
        if (now - lastTime > wait) {
            // 重置上一次执行该函数的时间
            lastTime = now;
            func.apply(this, args);
        }
    };
};

实现效果:

上方输入框,下方显示区。不断输入内容时,每隔500ms,下方显示区才会更新一次内容

@zhengyongkai
Copy link

😃😃😃 本人能力有限,欢迎大佬指正改进~

1. 手写防抖函数(debounce)

防抖函数功能:

在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时

比如一个搜索框,应用防抖函数后,当用户不断输入内容时,不会发送请求。只有当用户一段时间T内不输入内容了,才会发送一次请求。如果小于这段时间T继续输入内容的话,就会重新计算时间T,也不会发送请求。这样降低了发送请求的次数,提高性能的同时也提升了用户体验。

实现防抖函数:

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

实现效果:

上方输入框,下方显示区,不断输入内容时,下方显示区不会更新。只有在1s内不输入内容了,下方显示区才会更新内容。

2. 手写节流函数(throttle)

节流函数功能:

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

实现节流函数:

// func是用户传入需要防抖的函数
// wait是等待时间,若未传参,默认50ms
const throttle = (func, wait = 50) => {
    // 上一次执行该函数的时间
    let lastTime = 0;
    // 返回的函数是每次用户实际调用的节流函数
    return (...args) => {
        // 获取当前时间,并转化为number
        let now = +new Date();
        // 将当前时间和上一次执行函数时间对比
        // 如果差值大于设置的等待时间就执行函数
        if (now - lastTime > wait) {
            // 重置上一次执行该函数的时间
            lastTime = now;
            func.apply(this, args);
        }
    };
};

实现效果:

上方输入框,下方显示区。不断输入内容时,每隔500ms,下方显示区才会更新一次内容

我感觉节流应该是用在滚动条 防抖用在输入框 突然觉得防抖这个词有点可以理解为百度输入查询时候下面那个查询弹出框为了不然他一直抖动....

@Ha0ran2001
Copy link

// 防抖
function debounce(wait, fn, immediate) {
  let timeout, result;
  let debounced = function () {
    let args = arguments;
    let context = this;

    if (timeout) {
      clearTimeout(timeout);
    }

    // 立即执行
    if (immediate) {
      let callNow = !timeout;

      timeout = setTimeout(() => {
        timeout = null;
      }, wait);

      if (callNow) result = fn.apply(context, args);
    } else {
      timeout = setTimeout(() => {
        fn.apply(context, args);
      }, wait);
    }

    return result;
  }

  debounced.cancel = function () {
    clearTimeout(timeout);
    timeout = null;
  }
}
// 节流
function throttle(wait, fn) {
  let timeout, previous;

  return function () {
    const args = arguments;
    const context = this;
    let now = +new Date();
    // 剩余时间 大于0表示还没超过wait,除非第一次否则不执行
    let remaining = wait - (now - previous);
    // remaining < wait 的情况是你修改了系统的时间会出现的
    // 第一次立即执行
    if (remaining <= 0 | remaining < wait) {
      // 这个 if 判断是防止定时器不准确,比如设置的 2s 但是 2.2s 才执行,那么这个判断就起作用了
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }

      previous = now;

      fn.apply(context, args);

      // 最后一次还要执行
    } else if (!timeout) {
      timeout = setTimeout(() => {
        fn.apply(context, args);
        previous = +new Date();
        timeout = null;
      }, remaining);
    }
  }
}

@Ha0ran2001
Copy link

@wangziqi0503
Copy link

wangziqi0503 commented Mar 21, 2022 via email

@Yangfan2016
Copy link

防抖 一定时间内触发的事件合成一个事件进行触发 ,常用于 搜索框的输入联想提示 功能

截流 一定时间内触发的事件 按 一定时间规律性的触发,常用于 无限滚动加载

@xiaogu-123
Copy link

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

我用 vue 试了一下,请大佬们指点:
防抖

data () {
   return {
     timer: null  // 创建一个标记用来存放定时器的返回值
   }
 },
methods: {
    // 思路:每次触发事件时都取消之前的延时调用方法      

    debounce () {
      if (this.timer) { clearTimeout(this.timer) }

      // 创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内,如果还有字符输入的话,就不会执行 函数体内容
      this.timer = setTimeout(() => {
       console.log(1)
        this.$message('这是一条 防抖 消息提示')
      }, 1000)
    }
  }

节流

data () {
   return {
     canRun: true  // 通过闭包保存一个标记
   }
 },
 methods: {
   // 思路: 每次触发事件时,都判断当前是否有,等待执行的延时函数

   throttle () {
     if (!this.canRun) return

     this.canRun = false
     setTimeout(() => {
       console.log(2)
       this.$message('这是一条 节流 消息提示')
       // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远>是false,在开头被return掉
       this.canRun = true
     }, 3000)
   }
 }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests