理解函数防抖Debounce

栏目: JavaScript · 发布时间: 6年前

内容简介:有如下代码当我们在PC上滚动页面时,一秒可以轻松触发30次事件。在手机上进行测试时,一秒触发事件可以达到100次甚至更多。这里的回调函数只是打印字符串,如果回调函数更加复杂,可想而知浏览器的压力会非常大,用户体验会很糟糕。

有如下代码

window.onscroll = () => {
  console.log('触发滚动监听回调函数')
}
复制代码

当我们在PC上滚动页面时,一秒可以轻松触发30次事件。在手机上进行测试时,一秒触发事件可以达到100次甚至更多。

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

resizescroll 等Dom事件的监听回调会被频繁触发,因此我们要对其进行限制。

二、实现思路

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

第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。

三、Debounce 应用场景

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

四、函数防抖最终版

代码说话,有错恳请指出

function debounce(method, wait, immediate) {
  let timeout
  // debounced函数为返回值
  // 使用Async/Await处理异步,如果函数异步执行,等待setTimeout执行完,拿到原函数返回值后将其返回
  // args为返回函数调用时传入的参数,传给method
  let debounced = async function(...args) {
    // 用于记录员原函数执行结果
    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)
      }
    } else {
      // 如果immediate为false,则等待函数执行并记录其执行结果
      // 并将Promise状态置为fullfilled,以使函数继续执行
      await new Promise(resolve => {
        timeout = setTimeout(() => {
          // args是一个数组,所以使用fn.apply
          // 也可写作method.call(context, ...args)
          result = method.apply(context, args)
          resolve()
        }, wait)
      })
    }
    // 将原函数执行结果返回
    return result
  }

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

  return debounced
}
复制代码

需要注意的是如果传入的 immediate 参数为 false 时,调用防抖后的函数的外层函数也需要使用Async/Await语法等待执行结果返回

使用方法见代码:

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

let debouncedFn = debounce(square, 1000, false)

window.addEventListener('scroll', async () => {
  let val = await debouncedFn(4)
  // 停止滚动1S后输出:
  // 原函数的返回值为:16
  console.log(`原函数返回值为${val}`)
}, false)
复制代码

具体的实现步骤请往下看

五、Debounce 的实现

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

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

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

window.onscroll = 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函数。

六、遗留问题

需要防抖的函数可能是存在返回值的,我们要对这种情况进行处理, 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('scroll', () => {
  debouncedFn(4)
}, false)

// 停止滚动1S后输出:
// 原函数的返回值为:16
复制代码

2. 使用Promise处理返回值

function debounce(method, wait, immediate) {
  let timeout, result
  let debounced = function(...args) {
    // 返回一个Promise,以便可以使用then调用原函数返回值
    return new Promise((resolve, reject) => {
      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('scroll', () => {
  debouncedFn(4).then(val => {
    console.log(`原函数的返回值为:${val}`)
  })
}, false)

// 停止滚动1S后输出:
// 原函数的返回值为:16
复制代码

3. 使用Async/Await处理返回值

function debounce(method, wait, immediate) {
  let timeout, result
  // 使用Async/Await处理异步,如果函数异步执行,等待setTimeout执行完,拿到原函数返回值后将其返回
  let debounced = async 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)
      }
    } else {
      // 如果immediate为false,则等待函数执行并记录其返回值
      // 并将Promise状态置为fullfilled,以使函数继续执行
      await new Promise(resolve => {
        timeout = setTimeout(() => {
          result = method.apply(context, args)
          resolve()
        }, wait)
      })
    }
    return result
  }

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

  return debounced
}
复制代码

需要注意的是如果传入的 immediate 参数为 false 时,调用防抖后的函数的外层函数也需要使用Async/Await语法等待执行结果返回

使用方法见代码:

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

let debouncedFn = debounce(square, 1000, false)

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

// 停止滚动1S后输出:
// 原函数的返回值为:16
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

众媒时代

众媒时代

腾讯传媒研究 / 中信出版集团股份有限公司 / 2016-3-1 / CNY 52.00

众媒时代,就是一个大众参与的媒体时代。互联网将传统媒体垄断而单一的传播方式彻底颠覆。人人都可以通过互联网成为内容的制造者、传播者。每个人都是媒体,人是种子,媒体变成了土壤。 当我们的信息入口被朋友圈霸占,当我们的眼睛只看得到10W+,当我们不可抑制地沉浸在一次次的“技术狂欢”中,当人人都可以举起手机直播突发现场,当未来的头条由机器人说了算……内容正生生不息地以各种可能的形式出现,我们正彻头彻......一起来看看 《众媒时代》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具