手写防抖与节流 | 刷题打卡

吐槽君 分类:javascript

手写防抖与节流

闲时要有吃紧的心思,忙时要有悠闲的趣味

  • 掘金团队号上线,助你 Offer 临门! 点击 查看详情

目录

  • 前言

  • 正文

    • 一、防抖(debounce)
    • 二、节流 (throttle)
  • 总结

  • 参考文档

前言

返回目录

 面试必问,不说废话。

正文

一、防抖(debounce)

返回目录

 防抖,顾名思义,防止抖动,以免把一次事件误认为多次,敲键盘就是一个每天都会接触到的防抖操作。

原理

  • 某个任务你并不想它太过频繁触发,那么设置一个指定间隔的定时器来延迟执行;
  • 每次进来的时候都清除原本的定时器,然后重新开始计时;
  • 只有任务触发的间隔超过指定间隔的时候,任务才会执行。

特点

  • 如果在指定间隔内(如 1000ms)再次触发任务,那么当前的计时取消,所有任务不会执行,重新开始计时
  • 如果在指定间隔内(如 1000ms)没有再次触发任务,那么就执行最后一次任务

 也就是说,如果触发太过频繁,会导致一次响应都没有。只能等你最后一次触发结束指定间隔(如 1000ms)后,才能执行最后一次任务。

应用场景

  • 登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
  • 调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
  • 文本编辑器实时保存,当无任何更改操作一秒后进行保存

代码实现

「防抖重在清零 clearTimeout(timer)」

/**
 * 防抖
 * @param {String} fn 回调方法
 * @param {String} delay 缓冲时间
 */

function debounce(fn, delay) {
  // 创建一个标记用来存放定时器
  let timeout = null
  return function () {
    // 每次函数触发的时候,清空之前的定时器
    clearTimeout(timeout)
    // 创建一个新的 setTimeout
    // 这样就能保证点击按钮后的 delay 时间间隔内
    // 如果用户还点击了的话,就不会执行 fn 函数
    timeout = setTimeout(() => {
      // 使用apply修正 this 指向,执行传入函数
      fn.apply(this, arguments)
    }, delay)
  }
}
 

二、节流 (throttle)

 节流,顾名思义,控制水的流量。控制事件发生的频率,如控制为 1s 发生一次,甚至 1 分钟发生一次。与服务端(server)及网关(gateway)控制的限流 (Rate Limit) 类似。

原理

  • 初始化一个开关锁为真
  • 设置闭包函数,函数内判断标记为真才执行,然后将标记置为假
  • 闭包函数内一个指定间隔的定时器来延迟执行任务,执行完则再将标记置真;

特点

  • 无论在指定间隔内(如 1000ms)触发多少次任务,只执行第一次任务
  • 在触发任务的指定间隔(如 1000ms)之后,肯定执行第一次任务。

 也就是说,只要你触发一次任务,每在指定间隔(如 1000ms)之后肯定执行且只执行第一次;循环往复。

应用场景

  • scroll 事件,每隔一秒计算一次位置信息等
  • 浏览器播放事件,每个一秒计算一次进度信息等
  • input 框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求 (也可做防抖)

代码实现

「节流重在开关锁」

/**
 * 节流
 * @param {String} fn 回调方法
 * @param {String} delay 缓冲时间
 */

function throttle(fn, delay) {
  // 初始化一个状态为真
  let canRun = true
  return function () {
    // 判断状态,休息时间 暂不接客
    if (!canRun) {
      return
    }
    // 工作时间,执行函数;
    // 在间隔期内把状态位设为假
    canRun = false
    // 创建定时器,延迟执行任务
    setTimeout(() => {
      // 使用apply修正 this 指向,执行传入函数
      fn.apply(this, arguments)
      // 执行完任务之后,重新将这个标志设置为真
      canRun = true
    }, delay)
  }
}
 

总结

返回目录

 开始手写系列。

 路漫漫其修远兮,与诸君共勉。

参考文献

  • jsliang 求职系列 - 12 - 手写防抖和节流 | 掘金-jsliang
  • 浅谈 JS 防抖和节流 | 思否-安歌

附演示代码

<div>
  <button id="debounceBtn">点我防抖</button>
  <button id="throttleBtn">点我节流</button>
</div>

<script>
  // 点击按钮次数
  let clickNum = 0
  // 按钮点击时间
  let clickTime = 0
  // 按钮开始点击时间
  let startTime = 0
  // 执行时间
  let executionTime = 0
  // 获取按钮
  const debounceBtn = document.getElementById('debounceBtn')
  const throttleBtn = document.getElementById('throttleBtn')
  // 绑定点击事件
  debounceBtn.onclick = function (e) {
    clickBtn()
    debounceFn(clickNum)
  }
  throttleBtn.onclick = function (e) {
    clickBtn()
    throttleFn(clickNum)
  }

  // 节流及防抖事件
  let debounceFn = debounce(fnExecution, 1000)
  let throttleFn = throttle(fnExecution, 1000)

  // 点击按钮事件
  function clickBtn() {
    clickNum++
    clickTime = Date.now()
    if (clickNum === 1) {
      startTime = Date.now()
    }
    console.log(`这是第${clickNum}次点击`)
  }

  function fnExecution(num) {
    executionTime = Date.now()
    console.log(
      `生效的是第 ${num}次点击,生效时间与最后一次点击隔时间为${
        executionTime - clickTime
      }毫秒,生效时间与第一次点击隔时间为${executionTime - startTime}毫秒`
    )
    clickNum = 0
  }

  /**
   * 防抖
   * @param {String} fn 回调方法
   * @param {String} delay 缓冲时间
   */

  function debounce(fn, delay) {
    // 创建一个标记用来存放定时器
    let timeout = null
    return function () {
      // 每次函数触发的时候,清空之前的定时器
      clearTimeout(timeout)
      // 创建一个新的 setTimeout
      // 这样就能保证点击按钮后的 delay 时间间隔内
      // 如果用户还点击了的话,就不会执行 fn 函数
      timeout = setTimeout(() => {
        // 使用apply修正 this 指向,执行传入函数
        fn.apply(this, arguments)
      }, delay)
    }
  }

  /**
   * 节流
   * @param {String} fn 回调方法
   * @param {String} delay 缓冲时间
   */

  function throttle(fn, delay) {
    // 初始化一个状态为真
    let canRun = true
    return function () {
      // 判断状态,休息时间 暂不接客
      if (!canRun) {
        return
      }
      // 工作时间,执行函数;
      // 在间隔期内把状态位设为假
      canRun = false
      // 创建定时器,延迟执行任务
      setTimeout(() => {
        // 使用apply修正 this 指向,执行传入函数
        fn.apply(this, arguments)
        // 执行完任务之后,重新将这个标志设置为真
        canRun = true
      }, delay)
    }
  }
</script>
 

后记:Hello 小伙伴们,如果觉得本文还不错,记得点个赞或者给个 star,你们的赞和 star 是我编写更多更丰富文章的动力!GitHub 地址

文档协议

知识共享许可协议
db 的文档库 由 db 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/danygitgit上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。

回复

我来回复
  • 暂无回复内容