节流防抖及其应用(搜索框和连续滚动优化)

7,133 阅读5分钟

看完本文,可以解决下面三个面试题

  1. 手写一个节流、防抖函数并讲解原理
  2. 手写一个简单搜索框,并实现搜索逻辑
  3. 手写一个滚动优化

本文配有完整的 demo 演示,可前往逐一测试,使用 netlify 部署,访问速度可能稍慢。

lxfriday-give-me-job-web-serve.netlify.com/

节流防抖是一种防止函数频繁无序执行的设计思想,它的核心目的是让无序变为有序,让函数执行更符合预期目的。

防抖

防抖的原理是,只要在倒计时的范围内,新触发防抖函数就会导致计时器重置,要重新等待 wait 时长之后才能执行。

简单实现

最简单的 debounce

示例中绑定了 this,在调试工具中可以看到 this 和 event 对象

function debounce(func, wait) {
  let timeout
  return function debounced(...args) {
    const ctx = this
    if (timeout) clearTimeout(timeout)
    timeout = setTimeout(() => {
      func.apply(ctx, args)
    }, wait)
  }
}

需要立即执行的防抖

debounce2 with immediate

和上面不同之处在于,在定时时间之后,只要触发就会立即执行(第一种是要等待 wait 时长),然后至少在 wait 之后才能进行下次触发。

function debounce(func, wait, immediate) {
  let timeout
  return function debounced(...args) {
    const ctx = this
    if (timeout) clearTimeout(timeout)
    if (immediate) {
      const callNow = !timeout
      timeout = setTimeout(() => {
        timeout = null
      }, wait)
      if (callNow) func.apply(ctx, args)
    } else {
      timeout = setTimeout(() => {
        func.apply(ctx, args)
      }, wait)
    }
  }
}

带有取消功能的防抖

取消功能实际是取消计时器。

上述两种防抖都用到了计时器,第一种如果触发防抖函数,在 wait 时间内取消(也就是 setTimeout 的匿名函数还没有执行),则和没有触发的效果一致,也就相当于什么都没做。移到灰色区域,然后马上点击取消,页面没有变化。

第二种由于只要触发防抖就会导致回调被调用,则它的取消就相当于把下一次触发的间隔给取消了,也就是没有取消的时候还需要 wait 时长才能执行回调,取消了计时器,马上就可触发防抖函数进而直接执行回调,然后再次进入倒计时。

function debounce(func, wait, immediate) {
  let timeout
  function debounced(...args) {
    const ctx = this
    if (timeout) clearTimeout(timeout)

    if (immediate) {
      const callNow = !timeout
      timeout = setTimeout(() => {
        // 这里只有在 wait 时长之后,timeout 为 null,然后触发防抖函数才能立即执行,
        // 否则 callNow 为 false,不会立即执行,计时器会重新计时
        timeout = null
      }, wait)
      if (callNow) func.apply(ctx, args)
    } else {
      timeout = setTimeout(() => {
        func.apply(ctx, args)
      }, wait)
    }
  }
  debounced.cancel = function cancel() {
    clearTimeout(timeout)
    timeout = null
  }
  return debounced
}

我玩了会带取消的防抖,然后...

似乎在玩 debounce 中找到了乐趣。

防抖的特点是需要等待 wait 时长才能执行回调,游戏中放技能要读条比较符合防抖,释放技能需要先读条才能放出去,反复按技能键只会让技能读条一次次重来。

节流

节流分为时间戳节流和定时器节流,节流的特点是一直被触发时,每隔 wait 时长执行一次回调函数。 连续触发会按 wait 时长连续执行(这是和防抖的根本区别)。

时间戳节流

lxfriday-give-me-job-web-serve.netlify.com/throttle-ti…

时间戳节流,用最新触发的时间减去上一次回调执行的时间,如果大于等于 wait 则会执行回调,回调的执行依赖于当前节流函数被触发,鼠标不移动到灰色区域数字不可能变化。

时间戳节流在第一次触发时会立刻执行。

function throttle(func, wait) {
  let previous = 0
  return function throttled(...args) {
    const ctx = this
    const now = Date.now()
    const remain = wait - (now - previous)
    if (remain <= 0) {
      func.apply(ctx, args)
      previous = now
    }
  }
}

定时器节流

lxfriday-give-me-job-web-serve.netlify.com/throttle-ti…

定时器节流是在指定时间之后执行回调,触发一个定时器之后,即使再次触发节流函数,也不会导致定时器推迟执行(事件循环或者同步阻塞会导致延迟执行,在这里不用考虑)。它不会清除已经开始的定时器,而是等待定时器被执行之后才再开始下一个定时器。

function throttle(func, wait) {
  let timeout = 0
  return function throttled(...args) {
    const ctx = this
    // 如果已经是定时器定时阶段,则直接跳过,相当于忽略了触发
    // 必须等到定时器到时间之后
    if (!timeout) {
      timeout = setTimeout(() => {
        func.apply(ctx, args)
        timeout = null
      }, wait)
    }
  }
}

设计一个搜索框,并给出比较优雅的搜索反馈

以小米商城为例,搜索框下面能给出搜索建议,它具有以下几个特点:

  1. 输入框选中的时候会给出搜索建议
  2. 一直连续输入,在输入期间搜索建议不会变化,停下来马上能刷新建议
  3. 输入框清空的时候给出默认的搜索建议,同第一步的搜索建议

搜索结果的产生

  1. 点击回车来搜索
  2. 点击搜索建议来搜索

demo lxfriday-give-me-job-web-serve.netlify.com/debounce-se…

源码

  1. debounce-search.html
  2. debounce-search.js

示例的搜索框具备以下能力

  1. 搜索框选中时自动给出搜索提示
  2. 快速输入关键词时只会显示最后一次输入的关键词建议(右侧有内部的处理过程,内部时刻都在处理,没有显示是因为被防抖处理掉了)
  3. 点击关键词或者按回车进行搜索,对搜索进行了防抖处理,频繁按回车会一直推迟到最后一次按回车后的 0.5 秒显示搜索结果
  4. 回车搜索时,前后搜索的词相同则会自动阻止搜索

连续滚动优化

demo lxfriday-give-me-job-web-serve.netlify.com/debounce-sc…

源码

  1. debounce-scroll.html
  2. debounce-scroll.js

防抖处理过后效果非常明显,连续滚动时,左边弹窗会一直反馈,右边防抖的弹窗只在停下来了才弹出。

参考


欢迎大家关注我的掘金和公众号,算法、TypeScript、React 及其生态源码定期讲解。