函数防抖(debounce)

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

看一个🌰(栗子):

  1. //模拟一段ajax请求
  2. function ajax(content) {
  3. console.log('ajax request ' + content)
  4. }
  5. let inputa = document.getElementById('unDebounce')
  6. inputa.addEventListener('keyup', function (e) {
  7. ajax(e.target.value)
  8. })
  9. 复制代码

看一下运行结果:

5.8 - 防抖和节流 - 图1

可以看到,我们只要按下键盘,就会触发这次ajax请求。不仅从资源上来说是很浪费的行为,而且实际应用中,用户也是输出完整的字符后,才会请求。下面我们优化一下:

  1. //模拟一段ajax请求
  2. function ajax(content) {
  3. console.log('ajax request ' + content)
  4. }
  5. function debounce(fun, delay) {
  6. return function (args) {
  7. let that = this
  8. let _args = args
  9. clearTimeout(fun.id)
  10. fun.id = setTimeout(function () {
  11. fun.call(that, _args)
  12. }, delay)
  13. }
  14. }
  15. let inputb = document.getElementById('debounce')
  16. let debounceAjax = debounce(ajax, 500)
  17. inputb.addEventListener('keyup', function (e) {
  18. debounceAjax(e.target.value)
  19. })
  20. 复制代码

看一下运行结果:

5.8 - 防抖和节流 - 图2

可以看到,我们加入了防抖以后,当你在频繁的输入时,并不会发送请求,只有当你在指定间隔内没有输入时,才会执行函数。如果停止输入但是在指定间隔内又输入,会重新触发计时。 再看一个🌰:

  1. let biu = function () {
  2. console.log('biu biu biu',new Date().Format('HH:mm:ss'))
  3. }
  4. let boom = function () {
  5. console.log('boom boom boom',new Date().Format('HH:mm:ss'))
  6. }
  7. setInterval(debounce(biu,500),1000)
  8. setInterval(debounce(boom,2000),1000)
  9. 复制代码

看一下运行结果:

5.8 - 防抖和节流 - 图3

这个🌰就很好的解释了,如果在时间间隔内执行函数,会重新触发计时。biu会在第一次1.5s执行后,每隔1s执行一次,而boom一次也不会执行。因为它的时间间隔是2s,而执行时间是1s,所以每次都会重新触发计时

个人理解 函数防抖就是法师发技能的时候要读条,技能读条没完再按技能就会重新读条

源码分析

  1. function fn(ev) {
  2. console.log('OK');
  3. }
  4. // 返回方法 执行函数 频率 是否第一次执行
  5. btn.onclick = debounce(fn, 300, true);
  6. // box.onclick = proxy 疯狂触发 proxy
  7. // 但是我们最终想执行的是fn,
  8. // 所以需要我们在proxy中,基于一些逻辑的处理,让fn只执行一次即可
  9. /*
  10. * debounce:函数防抖 根据underscore代码编写
  11. * @params
  12. * func「function,required」:最后要执行的函数
  13. * wait「number」:设定的频发触发的频率时间,默认值是300
  14. * immediate「boolean」:设置是否是开始边界(第一次)触发,默认值是false
  15. * @return
  16. * func执行的返回结果
  17. */
  18. function debounce(func, wait, immediate) {
  19. // 必须得传函数
  20. if (typeof func !== "function") {
  21. throw new TypeError('func must be required and must be an function!');
  22. }
  23. // wait没写 (只写了true, 本意是传给immediate)
  24. if (typeof wait === "boolean") {
  25. immediate = wait;
  26. wait = 300;
  27. }
  28. if (typeof wait !== "number") wait = 300;
  29. if (typeof immediate !== "boolean") immediate = false;
  30. // 上面的判断降低报错可能
  31. var timer = null,
  32. result;
  33. // 返回方法 proxy (形成闭包)
  34. return function proxy() {
  35. var runNow = !timer && immediate, // 是否立即执行
  36. params = [].slice.call(arguments), // 函数里面有参数这里操作相当于(...参数)
  37. self = this; // this指向当前操作元素
  38. if (timer) clearTimeout(timer); // 频繁触发时候调用的清除
  39. timer = setTimeout(function () {
  40. if (timer) { //当最后一次的结束后,把没用的这个定时器也干掉「良好的习惯」
  41. clearTimeout(timer);
  42. timer = null;
  43. };
  44. // 能到这里(是最后执行)runNow是false(timer在频繁触发过程中有值, runNow就为false)
  45. // immediate才能保持不变
  46. !immediate ? result = func.apply(self, params) : null; // 最后执行
  47. }, wait);
  48. runNow ? result = func.apply(self, params) : null; // 是否立即执行
  49. return result;
  50. };
  51. }

简单版:

  1. let debounce = function(fn, delay = 500) {
  2. if (typeof fn !== "function") {
  3. throw new TypeError("Expected a function");
  4. }
  5. let timer = null;
  6. return (...arg) => {
  7. if (timer) {
  8. clearTimeout(timer);
  9. }
  10. timer = setTimeout(function() {
  11. fn.call(arg);
  12. }, delay);
  13. };
  14. };

核心代码:

  1. 频繁触发debounce, 每次都会重置定时器
  2. 重置定时器要
  3. **clearTimeout(timer);** (银行办业务,系统叫号, 销毁)
  4. **timer = null;** (银行办业务, 手里的纸条, 撕毁)
  5. 这里不需要,因为下面还要赋值
  6. 不在delay时间内触发debounce, 就执行一次函数
  1. function debounce(func, delay) {
  2. let timer = null;
  3. // 闭包一直在
  4. return function (...args) {
  5. // 频繁触发该函数,这里会一直clear计时器
  6. if (timer) clearTimeout(timer);
  7. // 当不再触发,delay毫秒后,就执行里面的函数
  8. timer = setTimeout(() => {
  9. //最后一次,能够执行了,也要记得清除最后一个定时器
  10. if (timer) {
  11. clearTimeout(timer);
  12. timer = null;
  13. };
  14. // func的this指向 this
  15. func.apply(this, args);
  16. }, delay)
  17. }
  18. }

函数节流(throttle)

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

看一个🌰:

  1. function throttle(fun, delay) {
  2. let last, deferTimer
  3. return function (args) {
  4. let that = this
  5. let _args = arguments
  6. let now = +new Date()
  7. if (last && now < last + delay) {
  8. clearTimeout(deferTimer)
  9. deferTimer = setTimeout(function () {
  10. last = now
  11. fun.apply(that, _args)
  12. }, delay)
  13. }else {
  14. last = now
  15. fun.apply(that,_args)
  16. }
  17. }
  18. }
  19. let throttleAjax = throttle(ajax, 1000)
  20. let inputc = document.getElementById('throttle')
  21. inputc.addEventListener('keyup', function(e) {
  22. throttleAjax(e.target.value)
  23. })
  24. 复制代码

看一下运行结果:

5.8 - 防抖和节流 - 图4

可以看到,我们在不断输入时,ajax会按照我们设定的时间,每1s执行一次。

结合刚刚biubiubiu的🌰:

  1. let biubiu = function () {
  2. console.log('biu biu biu', new Date().Format('HH:mm:ss'))
  3. }
  4. setInterval(throttle(biubiu,1000),10)
  5. 复制代码

5.8 - 防抖和节流 - 图5

不管我们设定的执行时间间隔多小,总是1s内只执行一次。

个人理解 函数节流就是fps游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。

  1. /*
  2. * throttle:函数节流
  3. * @params
  4. * func「function,required」:最后要执行的函数
  5. * wait「number」:设定的频发触发的频率时间,默认值是300
  6. * @return
  7. * func执行的返回结果
  8. */
  9. function throttle(func, wait) {
  10. if (typeof func !== "function") {
  11. throw new TypeError('func must be required and be an function!');
  12. }
  13. if (typeof wait !== "number") wait = 300;
  14. var timer = null,
  15. previous = 0,
  16. result;
  17. return function proxy() {
  18. var now = +new Date(),
  19. remaining = wait - (now - previous), // 剩下时间
  20. self = this,
  21. params = [].slice.call(arguments);
  22. // 超过时间了
  23. if (remaining <= 0) {
  24. // 立即执行即可
  25. if (timer) {
  26. clearTimeout(timer);
  27. timer = null;
  28. }
  29. result = func.apply(self, params);
  30. previous = +new Date(); // 刷新当前时间
  31. }
  32. // 还没超时, 继续等
  33. else if (!timer) {
  34. // 没有达到间隔时间,而且之前也没有设置过定时器,此时我们设置定时器,等到remaining后执行一次
  35. timer = setTimeout(function () {
  36. if (timer) {
  37. clearTimeout(timer);
  38. timer = null;
  39. }
  40. result = func.apply(self, params);
  41. previous = +new Date(); // 刷新当前时间
  42. }, remaining);
  43. }
  44. return result;
  45. };
  46. }

简单版

侧重于一段时间内,执行一次。

  1. let throttle = function(fn, delay = 400) {
  2. if (typeof fn !== "function") {
  3. throw new TypeError("Expected a function");
  4. }
  5. let flag = true;
  6. return function(...args) {
  7. if (!flag) return;
  8. flag = false;
  9. setTimeout(() => {
  10. fn(...args);
  11. flag = true;
  12. }, delay);
  13. };
  14. };

过程分析:

  1. 第一次触发事件,,开关项置为false
  2. delay时间段内,再次触发,不新计时
  3. delay时间后执行函数,并设置开关为true
  4. 再次触发事件,开关项置为false
  5. 距离上次触发delay时间内,不执行函数
  6. 距离上次触发delay时间后,执行函数,并设置开关true

总结

  • 函数防抖和函数节流都是防止某一时间频繁触发,但是这两兄弟之间的原理却不一样。
  • 函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。

结合应用场景

  • debounce
    • search搜索联想,用户在不断输入值时,用防抖来节约请求资源 触发Ajax请求。
    • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
  • throttle
    • 重复点击事件,mousedown(单位时间内只触发一次)
    • scroll(监听滚动事件),比如是否滑到底部自动加载更多,用throttle来判断