JS节流和防抖函数的理解和实现

JS节流和防抖函数的理解和实现

  • 前言
  • 函数防抖(debounce)
    • 定义及解读
    • debounce实现
  • 函数节流(throttle)
    • 定义及解读
    • throttle实现
      • 使用时间戳
      • 使用定时器
      • 加强版 throttle(结合debouce)
    • underscore 源码解读
  • 结合应用场景
    • debounce
    • throttle
  • 参考
  • 拓展

前言

防抖(debounce)和节流(throttle)两词其实并非计算机领域的原生词语。追根溯源,防抖一词来自于弱电领域,指代的是消除外界对于开关扰动的技术。而节流来自于流体力学,指代的是限定流体流量的一种技术。由于JavaScript拥有事件驱动的特性,为避免事件频繁触发导致性能的损耗,防抖和节流这两种技术在JavaScript中亦被广泛应用。

函数防抖(debounce)

定义及解读

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

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

debounce实现

这是高程中的经典代码:

    function debounce(method, context) {
     
        clearTimeout(method.tId);
        method.tId = setTimeout(function () {
     
            method.call(context);
        }, 100)
    }

实现 1

// 实现 1
// fn 是需要防抖处理的函数
// wait 是时间间隔
function debounce(fn, wait = 50) {
     
    // 通过闭包缓存一个定时器 id
    let timer = null
    // 将 debounce 处理结果当作函数返回
    // 触发事件回调时执行这个返回函数
    return function(...args) {
     
      	// 如果已经设定过定时器就清空上一次的定时器
        if (timer) clearTimeout(timer)
      
      	// 开始设定一个新的定时器,定时器结束后执行传入的函数 fn
        timer = setTimeout(() => {
     
            fn.apply(this, args)
        }, wait)
    }
}

// DEMO
// 执行 debounce 函数返回新函数
const betterFn = debounce(() => console.log('fn 防抖执行了'), 1000)
// 停止滑动 1 秒后执行函数 () => console.log('fn 防抖执行了')
document.addEventListener('scroll', betterFn)

实现 2(立即执行)
上述实现方案已经可以解决大部分使用场景了,不过想要实现第一次触发回调事件就执行 fn 有点力不从心了,这时候我们来改写下 debounce 函数,加上第一次触发立即执行的功能。

function debounce(func, wait, scope, immediate = false) {
     
  var timer = null
  // (...args) 或者这里设置形参
  return function() {
     
    var context = scope || this,
      args = arguments
    var later = function() {
     
      func.apply(context, args)
    }
    clearTimeout(timer)
    // ------ 新增部分 start ------
    // immediate 为 true 表示第一次触发后执行
    // timer 为空表示首次触发
    if (immediate && !timer) {
     
      console.log('first call')
      later()
    }
    // ------ 新增部分 end ------
    timer = setTimeout(later, wait)
  }
}

测试案例

//测试
function fn() {
     
  console.log('fn')
}

setInterval(debounce(fn, 2000, true), 1900) // 更改定时器,即可查看效果

函数节流(throttle)

定义及解读

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

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

throttle实现

使用时间戳

// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
     
  // 上一次执行 fn 的时间
  let previous = 0
  // 将 throttle 处理结果当作函数返回
  return function(...args) {
     
    // 获取当前时间,转换成时间戳,单位毫秒
    let now = +new Date()
    // 将当前时间和上一次执行函数的时间进行对比
    // 大于等待时间就把 previous 设置为当前时间并执行函数 fn
    if (now - previous > wait) {
     
      previous = now
      fn.apply(this, args)
    }
  }
}

使用定时器

function throttle(func, wait, scope) {
     
  var timer = null
  return function() {
     
    var args = arguments
    var context = scope || this
    if (!timer) {
     
      timer = setTimeout(function() {
     
        timer = null
        func.apply(context, args)
      }, wait)
    }
  }
}

测试案例

//测试
function fn() {
     
  console.log('fn')
}

setInterval(throttle(fn, 200), 20)

加强版 throttle(结合debouce)

现在考虑一种情况,如果用户的操作非常频繁,不等设置的延迟时间结束就进行下次操作,会频繁的清除计时器并重新生成,所以函数 fn 一直都没办法执行,导致用户操作迟迟得不到响应

有一种思想是将「节流」和「防抖」合二为一,变成加强版的节流函数,关键点在于「 wait 时间内,可以重新生成定时器,但只要 wait 的时间到了,必须给用户一个响应」。这种合体思路恰好可以解决上面提出的问题。

结合 throttle 和 debounce 代码,加强版节流函数 throttle 如下,新增逻辑在于当前触发时间和上次触发的时间差小于时间间隔时,设立一个新的定时器,相当于把 debounce 代码放在了小于时间间隔部分。

function throttle(func, wait, scope) {
     
  wait || (wait = 250)
  var timer = null,
    last = 0
  return function() {
     
    var context = scope || this
    var now = +new Date(),
      args = arguments
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔
    if (now - last < wait) {
     
      if (timer) clearTimeout(timer)
      timer = setTimeout(function() {
     
        last = now
        func.apply(context, args)
      }, wait)
    } else {
     
      // 第一次执行
      // 或者时间间隔超出了设定的时间间隔,执行函数 fn
      last = now
      func.apply(context, args)
    }
  }
}

underscore 源码解读

上述代码实现了一个简单的节流函数,不过 underscore 实现了更高级的功能,即新增了两个功能

  • 配置是否需要响应事件刚开始的那次回调( leading 参数,false 时忽略)
  • 配置是否需要响应事件结束后的那次回调( trailing 参数,false 时忽略)

配置 { leading: false } 时,事件刚开始的那次回调不执行;配置 { trailing: false } 时,事件结束后的那次回调不执行,不过需要注意的是,这两者不能同时配置。

所以在 underscore 中的节流函数有 3 种调用方式,默认的(有头有尾),设置 { leading: false } 的,以及设置 { trailing: false } 的。上面说过实现 throttle 的方案有 2 种,一种是通过时间戳判断,另一种是通过定时器创建和销毁来控制。

第一种方案实现这 3 种调用方式存在一个问题,即事件停止触发时无法响应回调,所以 { trailing: true } 时无法生效。

第二种方案来实现也存在一个问题,因为定时器是延迟执行的,所以事件停止触发时必然会响应回调,所以 { trailing: false } 时无法生效。

underscore 采用的方案是两种方案搭配使用来实现这个功能。

const throttle = function(func, wait, options) {
     
  var timeout, context, args, result;
  
  // 上一次执行回调的时间戳
  var previous = 0;
  
  // 无传入参数时,初始化 options 为空对象
  if (!options) options = {
     };

  var later = function() {
     
    // 当设置 { leading: false } 时
    // 每次触发回调函数后设置 previous 为 0
    // 不然为当前时间
    previous = options.leading === false ? 0 : _.now();
    
    // 防止内存泄漏,置为 null 便于后面根据 !timeout 设置新的 timeout
    timeout = null;
    
    // 执行函数
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };

  // 每次触发事件回调都执行这个函数
  // 函数内判断是否执行 func
  // func 才是我们业务层代码想要执行的函数
  var throttled = function() {
     
    
    // 记录当前时间
    var now = _.now();
    
    // 第一次执行时(此时 previous 为 0,之后为上一次时间戳)
    // 并且设置了 { leading: false }(表示第一次回调不执行)
    // 此时设置 previous 为当前值,表示刚执行过,本次就不执行了
    if (!previous && options.leading === false) previous = now;
    
    // 距离下次触发 func 还需要等待的时间
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    
    // 要么是到了间隔时间了,随即触发方法(remaining <= 0)
    // 要么是没有传入 {leading: false},且第一次触发回调,即立即触发
    // 此时 previous 为 0,wait - (now - previous) 也满足 <= 0
    // 之后便会把 previous 值迅速置为 now
    if (remaining <= 0 || remaining > wait) {
     
      if (timeout) {
     
        clearTimeout(timeout);
        
        // clearTimeout(timeout) 并不会把 timeout 设为 null
        // 手动设置,便于后续判断
        timeout = null;
      }
      
      // 设置 previous 为当前时间
      previous = now;
      
      // 执行 func 函数
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
     
      // 最后一次需要触发的情况
      // 如果已经存在一个定时器,则不会进入该 if 分支
      // 如果 {trailing: false},即最后一次不需要触发了,也不会进入这个分支
      // 间隔 remaining milliseconds 后触发 later 方法
      timeout = setTimeout(later, remaining);
    }
    return result;
  };

  // 手动取消
  throttled.cancel = function() {
     
    clearTimeout(timeout);
    previous = 0;
    timeout = context = args = null;
  };

  // 执行 _.throttle 返回 throttled 函数
  return throttled;
};

结合应用场景

debounce

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。(input 输入回调事件添加防抖函数后,只会在停止输入后触发一次)
  • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

throttle

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
  • 例如:window.onresize() 事件、mousemove 事件、上传进度等情况

参考

节流/防抖动画演示网站
木易杨:深入浅出节流/防抖函数
7分钟理解JS的节流、防抖及使用场景
JavaScript专题之跟着 underscore 学节流
Js中的防抖与节流

拓展

这是动画演示的代码, 可以通过这个链接下载: 跳转

/**
 * Created by thephpjo on 21.04.14.
 */

var helpers = {
     
  /**
   * debouncing, executes the function if there was no new event in $wait milliseconds
   * @param func
   * @param wait
   * @param scope
   * @returns {Function}
   */
  debounce: function(func, wait, scope) {
     
    var timeout
    return function() {
     
      var context = scope || this,
        args = arguments
      var later = function() {
     
        timeout = null
        func.apply(context, args)
      }
      clearTimeout(timeout)
      timeout = setTimeout(later, wait)
    }
  },

  /**
   * in case of a "storm of events", this executes once every $threshold
   * @param fn
   * @param threshhold
   * @param scope
   * @returns {Function}
   */
  throttle: function(fn, threshhold, scope) {
     
    threshhold || (threshhold = 250)
    var last, deferTimer
    return function() {
     
      var context = scope || this

      var now = +new Date(),
        args = arguments
      if (last && now < last + threshhold) {
     
        // hold on to it
        clearTimeout(deferTimer)
        deferTimer = setTimeout(function() {
     
          last = now
          fn.apply(context, args)
        }, threshhold)
      } else {
     
        last = now
        fn.apply(context, args)
      }
    }
  }
}

你可能感兴趣的:(#,JS/TS,面试,javascript)