Skip to content

函数节流(throttle)

函数执行一次后,只有大于规定的时间后才会执行第二次, 适合多次事件按时间做平均分配触 发,比如 1s 内我们可以点击鼠标多次, 设置 wait 时间为 2s, 就算我们不停点击鼠标 10s, 也只会触发 6 次。

应用场景:

  • 1.窗口调整(resize)、页面滚动(scroll)

  • 2.DOM元素的拖拽(mousemove)

  • 3.抢购疯狂点击(click)

javascript
function throttle (fn, wait) {
  let prev = 0

  return function (...args) {
    let now = Date.now()

    if (now - prev > wait) {
      const result = func.apply(context, args)
      prev = now

      return result
    }
  }
}
javascript
/**
 * 函数节流
 * 
 * @param {Function} fn - 需要进行节流处理的原函数
 * @param {number} wait - 节流的时间间隔
 * @param {Object} options - 用于设置开始边界和结束边界是否触发的配置项
 * @param {boolean} options.leading - 开始边界是否触发
 * @param {boolean} options.trailing - 结束边界是否触发
 * @returns {Function} - 生成的节流函数
 */
const throttle = (fn, wait = 200, options = {}) => {
  const { leading, trailing } = options
  let context = null,
      parmas = null,
      result = null,
      timer = null,
      prev = 0

  // 结束边界触发的函数
  const later = () => {
    
    // 之后再次触发时当作第一次点击,如果开始边界要不触发,应该让 prev 为 0。
    prev = leading
      ? Date.now()
      : 0
    timer = null
    result = fn.call(context, ...parmas)
    
    if (!timer) {
      context = parmas = null
    }
  }

  const _throttle = function (...args) {
    parmas = args
    context = this

    // 记录点击的时间
    const now = Date.now()

    // 开始边界不触发,本来需要用一个变量标识是否是第一次点击,由于 prev 只有初始时的
    // 值是 0,正好可以用来标识是否是第一次点击。
    if (!prev && !leading) {
      prev = now
    }

    // 计算需要等待的时间
    const remaining = wait - (now - prev)

    if (remaining <= 0 || remaining > wait) { // remaining > wait 代表修改了时间

      // 清除之前因结束边界触发而设置的定时器,这种情况只会在结束边界定时器回调即将触
      // 发的同时又点击了一下时出现,此时正好超过等待时间。如果因时间误差先执行了点击
      // 时的代码,就会取消掉定时器回调;如果因时间误差先执行了定时器回调,prev 会更
      // 新,就不会执行下面的代码。总之保证了规定时间内只会执行一次。
      if (timer) {
        clearTimeout(timer)
        timer = null
      }

      prev = now
      result = fn.call(context, ...parmas)
      
      if (!timer) {
        context = parmas = null
      }

      return result
    } 
    
    // 结束边界触发
    if (!timer && trailing) {
      timer = setTimeout(later, remaining)

      return result
    }
  }

  // 取消
  _throttle.cancle = () => {
    clearTimeout(timer)
    prev = 0
    timer = context = parmas = null
  }

  return _throttle
}
javascript
const now = _.now()
const foo = () => {
  console.log(_.now() - now)
}
const bar = _.throttle(foo, 1000) // true false

setTimeout(() => {
  bar() // 立即触发,因为默认开始边界触发。
}, 0)

setTimeout(() => {
  bar() // 不触发,还没经过 1000 ms
}, 500)

setTimeout(() => {
  bar() // 1200 ms 时立即触发
}, 1200)

setTimeout(() => {
  bar() // 不触发
}, 1500)
javascript
const now = _.now()
const foo = () => {
  console.log(_.now() - now)
}
const bar = _.throttle(foo, 1000, { leading: false }) // false false

setTimeout(() => {
  bar() // 不触发,因为设置了开始边界不触发。
}, 0)

setTimeout(() => {
  bar() // 不触发,还没经过 1000 ms
}, 500)

setTimeout(() => {
  // 1200 ms 时触发,不会被当作新的开始来处理,只有开启结束边界触发时才会重置 prev。
  bar() 
}, 1200)

setTimeout(() => {
  bar() // 不触发,距离上次触发还没经过 1000 ms
}, 1500)
javascript
const now = _.now()
const foo = () => {
  console.log(_.now() - now)
}
const bar = _.throttle(foo, 1000, { trailing: true }) // true true


setTimeout(() => {
  bar() // 触发,因为设置了开始边界触发。
}, 0)

setTimeout(() => {
  bar() // 1000 ms 时触发,因为设置了结束边界触发
}, 500)

setTimeout(() => {
  // 被设置成了结束边界触发
  bar() 
}, 1200)

setTimeout(() => {
  bar() // 更新了结束边界触发,2000 ms 触发
}, 1500)

setTimeout(() => {
  bar() // // 被设置成了结束边界触发,3000 ms 触发
}, 2600)

setTimeout(() => {
  bar() // 距离上次触发超过了 1000ms,被当作了新的开始,4100 ms 触发。
}, 4100)
javascript
const now = _.now()
const foo = () => {
  console.log(_.now() - now)
}
const bar = _.throttle(foo, 1000, { leading: false, trailing: true }) // false true


setTimeout(() => {
  bar() // 不触发,因为设置了开始边界不触发。
}, 0)

setTimeout(() => {
  bar() // 1000 ms 时触发,因为设置了结束边界触发
}, 500)

setTimeout(() => {
  // 由于上次是结束边界触发,被当作了新的开始,此次点击时 prev 被置为 Date.now()
  bar() 
}, 1200)

setTimeout(() => {
  bar() // 2200 ms 触发,因为设置了结束边界触发
}, 1500)

setTimeout(() => {
  // 由于上次是结束边界触发,被当作了新的开始,此次点击时 prev 被置为 Date.now()
  bar() // 3600 ms 触发,因为设置了结束边界触发
}, 2600)

setTimeout(() => {
  // 由于上次是结束边界触发,被当作了新的开始,此次点击时 prev 被置为 Date.now()
  bar() // 5700 ms 触发,被当作了新的开始。
}, 4700)