nfssuzukaze / Blog

0 stars 0 forks source link

防抖与节流 #29

Open nfssuzukaze opened 3 years ago

nfssuzukaze commented 3 years ago

1. 防抖

对于频繁触发的事件, 如果浏览器也频繁的响应, 则会占用大量的进程资源, 导致卡顿. 而防抖就是为了防止浏览器因事件的频繁触发而频繁响应

原理: 对于一个触发的事件, 会在 n ms 后调用其回调函数, 如果这个事件在短时间内频繁触发, 则会在最后一次触发该事件时计时, 待 n ms 后执行回调函数

1.1 debounce 的初步实现

let debounce = function(func, wait) {
  // func 指绑定的函数, 而 wait 指的是等待调用回调函数的时间
  let timer = null

  return function(...args) {
    let self = this
    // 用 self 表示正确的 this 指向
    clearTimeout(timer)
    // 频繁触发时, 后一个事件的触发会导致前一个触发事件的 setTimeout 失效, 从而使得其无法执行 func
    timer = setTimeout(function() {
      func.call(self, ...args)
    }, wait)
  }
}

1.2 debounce 的进一步实现

然而有时候, 我们需要在第一次点击的时候就立即执行回调函数

let debounce = function(func, wait, immediate) {
  // func 指绑定的函数, 而 wait 指的是等待调用回调函数的时间
  // immediate 表示是否在(事件触发频繁的时间段)的开始, 执行 func 
  let timer = null

  return function(...args) {
    let self = this
    // 用 self 表示正确的 this 指向

    if (immediate) {
      if (!timer) func.call(self, ...args)
      // !timer 为 true(说明频繁触发并未开始) => 立即执行 func
      clearTimeout(timer)
      // 除去上一次的 setTimeout, 重新更新 timer = null 的时间
      timer = setTimeout(() => {timer = null}, wait)
      // 更新 timer, 防止短时间内再次执行的时候会立即执行 func
      // 过了 wait 时间后, 再使 timer = null, 此时触发事件就又可以立即执行了
    } else {
      clearTimeout(timer)
      // 频繁触发时, 后一个事件的触发会导致前一个触发事件的 setTimeout 失效, 从而使得其无法执行 func
      timer = setTimeout(function() {func.call(self, ...args)}, wait)
    }
  }
}

1.3 debounce 的代码简化

let debouce = function(func, wait, immediate) {
  let timer = null
  return function(...args) {
    let self = this

    if (immediate && !timer) func.call(self, ...args)

    clearTimeout(timer)
    timer = setTimeout(function() {
      if (!immediate) func.call(self, ...args)
      timer = null
      // 在 immediate 为 false 的时候, timer = null 对其没有影响
    }, wait)
  }
}

2. 节流

在一个时间段内, 无论触发多少次事件, 最终只会执行一次回调函数 image 如图所示, 在 wait 时间段内, 只会在 first trigger 时执行回调函数, 而在 after triggers 时则不会执行回调函数

2.1 throttle 的初步实现

let throttle = function(func, wait) {
  let previous = 0
  // 上一次执行回调函数的时间
  return function(...args) {
    let now = +new Date()
    if (now - previous > wait) {
      func.call(this, ...args)
      previous = now
      // 更新上一次触发事件的时间
    }
  }
}

2.2 throttle 的进一步实现

image 对照上图, 如果在 wait 时间段中已经有了 first trigger 的前提下, 还有一堆的 after triggers, 则这些 after triggers 中最后一个(记作 a)则会延迟到下一个 wait 时间段(记作 next_wait)作为 first trigger 触发 然而若是在 next_wait 的开始就有一个 first trigger(记作 b), 则会执行 b 而不执行 a

let throttle = function(func, wait) {
  let previous = 0
  // 上一次执行回调函数的时间
  let timer = null

  return function(...args) {
    let now = +new Date()
    let remaining = wait - (now - previous)
    // 触发事件时, 距离下一个 wait 的时间

    if (remaining <= 0) {
      func.call(this, ...args)
      previous = now
      clearTimeout(timer)
      // 防止在 b 执行后执行 a
    } else {
      // 如果还没到下一个时间段, 则使用计时器, 待到下一个时间段时执行回调函数
      clearTimeout(timer)
      timer = setTimeout(function() {
        func.call(this, ...args)
        previous = +new Date()
      }, remaining)
    }
  }
}