函数节流与防抖

429 阅读3分钟

本文是我对函数节流与防抖的一点粗浅理解,如有不对,恳请指正。

在事件触发频繁如 resize、scroll、mousemove 等场景中,为了减少处理开销,采用节流或防抖限制函数的执行次数。

都是在规定时间段内频繁触发事件只执行一次函数。

功能函数 fn 在定时器内 则在固定时间段 结束 时执行,类比吟唱施法时间,打断则重来。

功能函数 fn 在定时器外没有定时器 则在固定时间段 开始立即 执行,类比冷却时间,释放技能后进入冷却。

节流(throttle)

定时器版

立即执行版(常用)

功能函数在定时器外:触发事件后立即执行函数再进入规定冷却时间

function throttle(fn, delay = 500) { // 指定默认值
  let timer = null // 可以省略 =null
  return function() {
    if (!timer) {
      fn.apply(this, arguments) // 注意在定时器外(冷却)
      timer = setTimeout(() => {
        timer = null // 不可省略 
      }, delay)
    }
  }
}
window.onscroll = throttle(() => console.log('hi'), 1000) // 业务代码

代码链接

fn.apply(this, arguments) 使得 this 指向调用该功能函数 fn 的 DOM 元素。

思路:

  1. 每触发事件调用节流函数,如果没有计时器,就执行功能函数,并创建一个计时器,计时器计时完毕后清除自己。
  2. 在规定时间内不断调用节流函数,由于定时器已经存在,不触发功能函数 fn。

非立即执行版

功能函数在定时器内:触发事件后吟唱规定时间后执行函数

function throttle(fn, delay = 500) { // 指定默认值
  let timer = null // 可以省略 =null
  return function() {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null // 不可省略
        fn.apply(this, arguments) // 注意在定时器内(吟唱)
      }, delay)
    }
  }
}
window.onscroll = throttle(() => console.log('hi'), 1000) // 业务代码

时间戳版

触发事件后立即执行函数再进入规定冷却时间

function throttle(fn, delay = 500) {
  let previous = 0
  return function() {
    let now = Date.now()
    if (now - previous > delay) {
      fn.apply(this, arguments)
      previous = now
    }
  }
}
window.onscroll = throttle(() => console.log('hi'), 1000) // 业务代码

防抖(debounce)

非立即执行版(常用)

触发事件后吟唱规定时间后执行函数。

function debounce(fn, delay = 500) {
  let timer = null // 可以省略 =null
  return function() {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(this, arguments)
      timer = null // 可以省略改行
    }, delay)
  }
}

let input = document.querySelector('input')
input.oninput = debounce(() => {
  console.log(input.value) // 业务代码
}, 1000)

代码链接

思路:

  1. 每触发事件调用防抖函数就创建一个计时器,如果新创建计时器前存在一个计时器,就清除旧的计时器,保留新创建的计时器。
  2. 在规定时间内不断调用防抖函数,会不断创建新的计时器替代旧的计时器。某一计时器坚持规定时间不被替代,才执行功能函数。

通俗理解:像是擂台赛一样,谁在擂台上坚持一段时间,无人挑战,就是最终的胜出者。或像是拍卖一样,谁出价坚持一段时间,无人再次出价,就是最终的竞得者。

立即执行版

触发事件后立即执行函数,进入冷却时间。

function debounce(fn, delay = 500) {
  let timer = null  // 可以省略 =null
  return function() {
    let triggerNow = !timer // 判断

    if (timer) {
      clearTimeout(timer)
    }
    if (triggerNow) {
      fn.apply(this, arguments)
    }
    timer = setTimeout(() => {
      timer = null // 不可省略
    }, delay)
  }
}
let input = document.querySelector('input')
input.oninput = debounce(() => {
  console.log(input.value) // 业务代码
}, 1000)

合并:immediate 控制是否为立即执行版

function debounce(fn, delay = 500, immediate) {
  let timer = null  // 可以省略 =null
  return function() {
    if (timer) {
      clearTimeout(timer)
    }
    if (immediate) {
      const triggerNow = !timer // 判断
      timer = setTimeout(() => {
        timer = null // 不可省略
      }, delay)
      if (triggerNow) {
        fn.apply(this, arguments)
      }
    } else {
      timer = setTimeout(() => {
        fn.apply(this, arguments)
      }, delay);
    }
  }
}

let input = document.querySelector('input')
input.oninput = debounce3(() => {
  console.log(input.value) // 业务代码 
}, 1000, true)

总结

image.png

ES6 写法

节流(定时器立即执行版)

const throttle = (fn, delay = 500) => {
  let timer = null
  return (...args) => {
    if (timer) {
      return
    }
    fn.call(undefined, ...args)
    timer = setTimeout(() => {
      timer = null
    }, delay)
  }
}
window.onscroll = throttle(() => console.log('hi'), 1000) // 业务代码

防抖(定时器非立即执行版)

const debounce = (fn, delay = 500) => {
  let timer = null
  return (...args) => {
    if (timer !== null) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.call(undefined, ...args)
      timer = null
    }, delay)
  }
}
let input = document.querySelector('input')
input.oninput = debounce(() => {
  console.log(input.value) // 业务代码
}, 1000)

参考资料