理解函数防抖Debounce

1、函数为何要防抖

有以下代码git

window.onresize = () => {
  console.log('触发窗口监听回调函数')
}
复制代码

当咱们在PC上缩放浏览器窗口时,一秒能够轻松触发30次事件。手机端触发其余Dom时间监听回调时同理。github

这里的回调函数只是打印字符串,若是回调函数更加复杂,可想而知浏览器的压力会很是大,用户体验会很糟糕。数组

resizescroll等Dom事件的监听回调会被频繁触发,所以咱们要对其进行限制。浏览器

2、实现思路

函数去抖简单来讲就是对于必定时间段的连续的函数调用,只让其执行一次,初步的实现思路以下:app

第一次调用函数,建立一个定时器,在指定的时间间隔以后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另外一个。若是前一个定时器已经执行过了,这个操做就没有任何意义。然而,若是前一个定时器还没有执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求中止了一段时间以后才执行。异步

3、Debounce 应用场景

  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)

4、函数防抖最终版

代码说话,有错恳请指出async

function debounce(method, wait, immediate) {
  let timeout
  // debounced函数为返回值
  // 使用Async/Await处理异步,若是函数异步执行,等待setTimeout执行完,拿到原函数返回值后将其返回
  // args为返回函数调用时传入的参数,传给method
  let debounced = function(...args) {
    return new Promise (resolve => {
      // 用于记录原函数执行结果
      let result
      // 将method执行时this的指向设为debounce返回的函数被调用时的this指向
      let context = this
      // 若是存在定时器则将其清除
      if (timeout) {
        clearTimeout(timeout)
      }
      // 当即执行须要两个条件,一是immediate为true,二是timeout未被赋值或被置为null
      if (immediate) {
        // 若是定时器不存在,则当即执行,并设置一个定时器,wait毫秒后将定时器置为null
        // 这样确保当即执行后wait毫秒内不会被再次触发
        let callNow = !timeout
        timeout = setTimeout(() => {
          timeout = null
        }, wait)
        // 若是知足上述两个条件,则当即执行并记录其执行结果
        if (callNow) {
          result = method.apply(context, args)
          resolve(result)
        }
      } else {
        // 若是immediate为false,则等待函数执行并记录其执行结果
        // 并将Promise状态置为fullfilled,以使函数继续执行
        timeout = setTimeout(() => {
          // args是一个数组,因此使用fn.apply
          // 也可写做method.call(context, ...args)
          result = method.apply(context, args)
          resolve(result)
        }, wait)
      }
    })
  }

  // 在返回的debounced函数上添加取消方法
  debounced.cancel = function() {
    clearTimeout(timeout)
    timeout = null
  }

  return debounced
}
复制代码

须要注意的是,若是须要原函数返回值,调用防抖后的函数的外层函数须要使用Async/Await语法等待执行结果返回ide

使用方法见代码:函数

function square(num) {
  return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false)

window.addEventListener('resize', async () => {
  let val
  try {
    val = await debouncedFn(4)
  } catch (err) {
    console.error(err)
  }
  // 中止缩放1S后输出:
  // 原函数的返回值为:16
  console.log(`原函数返回值为${val}`)
}, false)
复制代码

具体的实现步骤请往下看post

5、Debounce 的实现

1. 《JavaScript高级程序设计》(第三版)中的实现

function debounce(method, context) {
  clearTimeout(method.tId)
  method.tId = setTimeout(() => {
    method.call(context)
  }, 1000)
}

function print() {
  console.log('Hello World')
}

window.onresize = debounce(print)
复制代码

咱们不停缩放窗口,当中止1S后,打印出Hello World。

有个能够优化的地方: 此实现方法有反作用(Side Effect),改变了输入值(method),给method新增了属性

2. 优化初版:消除反作用,将定时器隔离

function debounce(method, wait, context) {
  let timeout
  return function() {
    if (timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout(() => {
      method.call(context)
    }, wait)
  }
}
复制代码

3. 优化第二版:自动调整this正确指向

以前的函数咱们须要手动传入函数执行上下文context,如今优化将 this 指向正确的对象。

function debounce(method, wait) {
  let timeout
  return function() {
    // 将method执行时this的指向设为debounce返回的函数被调用时的this指向
    let context = this
    if (timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout(() => {
      method.call(context)
    }, wait)
  }
}
复制代码

4. 优化第三版:函数可传入参数

即使咱们的函数不须要传参,可是别忘了JavaScript 在事件处理函数中会提供事件对象 event,因此咱们要实现传参功能。

function debounce(method, wait) {
  let timeout
  // args为返回函数调用时传入的参数,传给method
  return function(...args) {
    let context = this
    if (timeout) {
      clearTimeout(timeout)
    }
    timeout = setTimeout(() => {
      // args是一个数组,因此使用fn.apply
      // 也可写做method.call(context, ...args)
      method.apply(context, args)
    }, wait)
  }
}
复制代码

5. 优化第四版:提供当即执行选项

有些时候我不但愿非要等到事件中止触发后才执行,我但愿马上执行函数,而后等到中止触发n毫秒后,才能够从新触发执行。

function debounce(method, wait, immediate) {
  let timeout
  return function(...args) {
    let context = this
    if (timeout) {
      clearTimeout(timeout)
    }
    // 当即执行须要两个条件,一是immediate为true,二是timeout未被赋值或被置为null
    if (immediate) {
      // 若是定时器不存在,则当即执行,并设置一个定时器,wait毫秒后将定时器置为null
      // 这样确保当即执行后wait毫秒内不会被再次触发
      let callNow = !timeout
      timeout = setTimeout(() => {
        timeout = null
      }, wait)
      if (callNow) {
        method.apply(context, args)
      }
    } else {
      // 若是immediate为false,则函数wait毫秒后执行
      timeout = setTimeout(() => {
        // args是一个类数组对象,因此使用fn.apply
        // 也可写做method.call(context, ...args)
        method.apply(context, args)
      }, wait)
    }
  }
}
复制代码

6. 优化第五版:提供取消功能

有些时候咱们须要在不可触发的这段时间内可以手动取消防抖,代码实现以下:

function debounce(method, wait, immediate) {
  let timeout
  // 将返回的匿名函数赋值给debounced,以便在其上添加取消方法
  let debounced = function(...args) {
    let context = this
    if (timeout) {
      clearTimeout(timeout)
    }
    if (immediate) {
      let callNow = !timeout
      timeout = setTimeout(() => {
        timeout = null
      }, wait)
      if (callNow) {
        method.apply(context, args)
      }
    } else {
      timeout = setTimeout(() => {
        method.apply(context, args)
      }, wait)
    }
  }

  // 加入取消功能,使用方法以下
  // let myFn = debounce(otherFn)
  // myFn.cancel()
  debounced.cancel = function() {
    clearTimeout(timeout)
    timeout = null
  }
}
复制代码

至此,咱们已经比较完整地实现了一个underscore中的debounce函数。

6、遗留问题

须要防抖的函数多是存在返回值的,咱们要对这种状况进行处理,underscore的处理方法是将函数返回值在返回的debounced函数内再次返回,可是这样实际上是有问题的。若是参数immediate传入值不为true的话,当防抖后的函数第一次被触发时,若是原始函数有返回值,实际上是拿不到返回值的,由于原函数是在setTimeout内,是异步延迟执行的,而return是同步执行的,因此返回值是undefined

第二次触发时拿到的返回值实际上是第一次执行的返回值,第三次触发时拿到的返回值实际上是第二次执行的返回值,以此类推。

1. 使用回调函数处理函数返回值

function debounce(method, wait, immediate, callback) {
  let timeout, result
  let debounced = function(...args) {
    let context = this
    if (timeout) {
      clearTimeout(timeout)
    }
    if (immediate) {
      let callNow = !timeout
      timeout = setTimeout(() => {
        timeout = null
      }, wait)
      if (callNow) {
        result = method.apply(context, args)
        // 使用回调函数处理函数返回值
        callback && callback(result)
      }
    } else {
      timeout = setTimeout(() => {
        result = method.apply(context, args)
        // 使用回调函数处理函数返回值
        callback && callback(result)
      }, wait)
    }
  }

  debounced.cancel = function() {
    clearTimeout(timeout)
    timeout = null
  }

  return debounced
}
复制代码

这样咱们就能够在函数防抖时传入一个回调函数来处理函数的返回值,使用代码以下:

function square(num) {
  return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false, val => {
  console.log(`原函数的返回值为:${val}`)
})

window.addEventListener('resize', () => {
  debouncedFn(4)
}, false)

// 中止缩放1S后输出:
// 原函数的返回值为:16
复制代码

2. 使用Promise处理返回值

function debounce(method, wait, immediate) {
  let timeout, result
  let debounced = function(...args) {
    // 返回一个Promise,以即可以使用then或者Async/Await语法拿到原函数返回值
    return new Promise(resolve => {
      let context = this
      if (timeout) {
        clearTimeout(timeout)
      }
      if (immediate) {
        let callNow = !timeout
        timeout = setTimeout(() => {
          timeout = null
        }, wait)
        if (callNow) {
          result = method.apply(context, args)
          // 将原函数的返回值传给resolve
          resolve(result)
        }
      } else {
        timeout = setTimeout(() => {
          result = method.apply(context, args)
          // 将原函数的返回值传给resolve
          resolve(result)
        }, wait)
      }
    })
  }

  debounced.cancel = function() {
    clearTimeout(timeout)
    timeout = null
  }

  return debounced
}
复制代码

使用方法一:在调用防抖后的函数时,使用then拿到原函数的返回值

function square(num) {
  return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false)

window.addEventListener('resize', () => {
  debouncedFn(4).then(val => {
    console.log(`原函数的返回值为:${val}`)
  })
}, false)

// 中止缩放1S后输出:
// 原函数的返回值为:16
复制代码

使用方法二:调用防抖后的函数的外层函数使用Async/Await语法等待执行结果返回

使用方法见代码:

function square(num) {
  return Math.pow(num, 2)
}

let debouncedFn = debounce(square, 1000, false)

window.addEventListener('resize', async () => {
  let val
  try {
    val = await debouncedFn(4)
  } catch (err) {
    console.error(err)
  }
  console.log(`原函数返回值为${val}`)
}, false)

// 中止缩放1S后输出:
// 原函数的返回值为:16
复制代码

7、参考文章

JavaScript专题之跟着underscore学防抖

underscore 函数去抖的实现

若是有错误或者不严谨的地方,请务必给予指正,十分感谢。

相关文章
相关标签/搜索