JavaScript 手写防抖函数(debounce)

424 阅读6分钟

认识防抖debounce函数

  • 我们来理解一下它的过程:
    • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间
    • 当事件密集触发时,函数的触发会被频繁的推迟
    • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数
  • 防抖的应用场景很多:
    • 输入框中频繁的输入内容,搜索或者提交信息;
    • 频繁的点击按钮,触发某个事件
    • 监听浏览器滚动事件,完成某些特定操作
    • 用户缩放浏览器的resize事件

防抖函数的案例

  • 举一个生活中常见的栗子

    • 就比如玩王者荣耀或者LOL时的回城功能,如果点击了回城没有其余打断操作的或,那么就会回城成功

    • 如果当你移动打断了回城,再进行回城时,就需要重新读条,一般这种功能就称之为是防抖

  • 或者我们都遇到过这样的场景,在某个搜索框中输入自己想要搜索的内容:

  • 比如想要搜索 nekoaimer

    • 当我输入n时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;
    • 当继续输入ne时,再次发送网络请求;
    • 那么nekoaimer一共需要发送9次网络请求;
    • 这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;
  • 但是我们需要这么多次的网络请求吗?

    • 不需要,正确的做法应该是在合适的情况下再发送网络请求;
    • 比如如果用户快速的输入一个nekoaimer,那么只是发送一次网络请求;
    • 比如如果用户是输入一个n想了一会儿,这个时候n确实应该发送一次网络请求;
    • 也就是我们应该监听用户在某个时间,比如500ms内,没有再次触发时间时,再发送网络请求;
  • 这就是防抖的操作:只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数;

Underscore 库的介绍

  • 事实上我们可以通过一些第三方库来实现防抖操作:
    • lodash
    • underscore
  • 这里使用underscore
    • 我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多;
    • 但是目前我看到underscore还在维护,lodash已经很久没有更新了;
  • Underscore的官网: underscorejs.org/
  • Underscore的安装有很多种方式:
    • 下载Underscore,本地引入;
    • 通过CDN直接引入;
    • 通过包管理工具(npm)管理安装;
  • 这里我们直接通过CDN引入:
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
  • HTML
<input type="text">
  • 500ms内有输入就不会触发
const inputEl = document.querySelector("input")
let counter = 0

inputEl.oninput = _.debounce((event) => console.log(`发送了${++counter}网络请求`, this, event), 500)

debounce v1 基本实现

  • HTML 结构
<input type="text">
const inputEl = document.querySelector("input")
let counter = 0
inputEl.oninput = debounce((event) => console.log(`发送了${++counter}网络请求`, this, event), 500)
  • Javascript 代码
function debounce(fn, delay) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null

  // 2.真正执行的函数
  const _debounce = function() {
    // 3.取消上一次的定时器
    if (timer) clearTimeout(timer)

    timer = setTimeout(() => {
      // 4.外部传入要执行的函数 
      fn()
    }, delay)
  }

  return _debounce
}
  • 这样也能实现, 但是this都是指向window,而且如果传了参数也没有处理

debounce v2 this参数

  • 实现this指向与参数传递
function debounce(fn, delay) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null

  // 2.真正执行的函数  并用args接收event对象或参数
  const _debounce = function(...args) {
    // 3.取消上一次的定时器
    if (timer) clearTimeout(timer)

    timer = setTimeout(() => {
      // 4.外部传入要执行的函数 传入this并将event对象或参数传入
      fn.apply(this, args)
    }, delay)
  }

  return _debounce
}

debounce v3 立即执行

  • 可以控制是否立即执行,默认不是立即执行
  • 这里的立即执行指的是:当第一次输入后,会立即执行一次,后续在不超过delay时不会执行,而在超过了delay时才会执行。然后过了一会儿如果你再接着输入就会又立即执行一次,而在超过了delay时才会执行。依次类推!
function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null

  // 2.判断是否立即执行过
  let isInvoke = false

  // 3.执行的函数
  const _debounce = function (...args) {
    
    // 4.取消上一次的定时器
    if (timer) clearTimeout(timer)
    
    // 5.每次第一次执行就会立即调用
    if (immediate && !isInvoke) {
      fn.apply(this, args)
      return isInvoke = true
    }
    
    // 6.如果不是第一次执行就会延时调用
    timer = setTimeout(() => { 
      fn.apply(this, args)
      isInvoke = false
    }, delay)
  }

  return _debounce
}

debounce v4 取消功能

  • 取消功能一般用于停止发送网络请求,比如:
    • 当用户输入信息还没到delay时,进行了关闭页面操作,或者推出登录操作
    • 此时就没有必要再向后端数据库发送请求增加服务器压力了
function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null

  // 2.判断是否立即执行过
  let isInvoke = false

  // 3.执行的函数
  const _debounce = function (...args) {
    
    // 4.取消上一次的定时器
    if (timer) clearTimeout(timer)
    
    // 5.每次第一次执行就会立即调用
    if (immediate && !isInvoke) {
      fn.apply(this, args)
      return isInvoke = true
    }
    
    // 6.如果不是第一次执行就会延时调用
    timer = setTimeout(() => { 
      fn.apply(this, args)
      isInvoke = false
    }, delay)
  }

  // 封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)

      // 初始化变量
      timer = null
      isInvoke = false
  }

  return _debounce
}
  • 这里可使用代码进行简单测试:
  • HTML测试代码
<input type="text">
<button id="cancel">取消</button>
  • js测试代码
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector("#cancel")

// 计数(网络请求次数)
let counter = 0

// 下面的oninput事件
const inputChange = (event) => console.log(`发送了${++counter}网络请求`, this, event)

// 拿到debounce返回值 _debounce函数
const debounceChange = debounce(inputChange, 500)

// oninput事件
inputEl.oninput = debounceChange

// 点击按钮取消事件
cancelBtn.onclick = function () {
  debounceChange.cancel()
}

debounce v5 函数返回值

思路一:回调函数

function debounce(fn, delay, immediate = false, resultCallback) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null

  // 2.判断是否立即执行过
  let isInvoke = false

  // 3.执行的函数
  const _debounce = function (...args) {
    // 4.取消上一次的定时器
    if (timer) clearTimeout(timer)
    
    // 5.每次第一次执行就会立即调用
    if (immediate && !isInvoke) {
      const result = fn.apply(this, args)

      // 6.如果传入了回调函数 则将返回值传入给回调函数
      if(resultCallback) resultCallback(result)
      return isInvoke = true
    }
    
    // 7.如果不是第一次执行就会延时调用
    timer = setTimeout(() => { 
      const result = fn.apply(this, args)

      // 8.如果传入了回调函数 则将返回值传入给回调函数
      if(resultCallback) resultCallback(result)
      isInvoke = false
    }, delay)
  }
  // 封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)

    // 初始化变量
    timer = null
    isInvoke = false
  }

  return _debounce
}
  • 这里可使用代码对resultCallback实现的返回值功能进行简单测试:
  • HTML测试代码
<input type="text">
<button id="cancel">取消</button>
  • js测试代码
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector("#cancel")
// 计数(网络请求次数)
let counter = 0
// 下面的oninput事件
const inputChange = (event) => {
  console.log(`发送了${++counter}网络请求`, this, event)
  // 返回值:返回 0-99 随机一个属
  return ~~(Math.random() * 100)
}
// 拿到debounce返回值 _debounce函数
const debounceChange = debounce(inputChange, 500, false, function (res) {
  console.log("resultCallback的返回值结果:", res)
})
// oninput事件
inputEl.oninput = debounceChange
// 点击按钮取消事件
cancelBtn.onclick = function () {
  debounceChange.cancel()
}

思路二:Promise

function debounce(fn, delay, immediate = false, resultCallback) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null

  // 2.判断是否立即执行过
  let isInvoke = false

  // 3.执行的函数
  const _debounce = function (...args) {
    // 4.通过返回Promise 传入返回结果
    return new Promise((resolve, reject) => {
        // 5.取消上一次的定时器
      if (timer) clearTimeout(timer)
      
      // 6.每次第一次执行就会立即调用
      if (immediate && !isInvoke) {
        const result = fn.apply(this, args)

        // 7.如果传入了回调函数 则将返回值传入resolve  抛出错误的话传给reject
        try {
          if(resultCallback && typeof resultCallback === 'function') resolve(result)
        } catch (err) {
          reject(err)
        }

        return isInvoke = true
      }
      
      // 8.如果不是第一次执行就会延时调用
      timer = setTimeout(() => { 
        const result = fn.apply(this, args)

        // 9.如果传入了回调函数 则将返回值传入resolve  抛出错误的话传给reject
        try {
          if(resultCallback && typeof resultCallback === 'function') resolve(result)
        } catch (err) {
          reject(err)
        }
        
        isInvoke = false
      }, delay)
    })
  }

  // 10.封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)

    // 11.初始化变量
    timer = null
    isInvoke = false
  }

  return _debounce
}
  • 这里可使用代码对Promise实现的返回值功能进行简单测试:
  • HTML测试代码
<input type="text">
<button id="cancel">取消</button>
  • js测试代码
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector("#cancel")

// 计数(网络请求次数)
let counter = 0

// 下面的oninput事件
const inputChange = (event) => {
  console.log(`发送了${++counter}网络请求`, this, event)
  // 返回值:返回 0-99 随机一个属
  return ~~(Math.random() * 100)
}

// 拿到debounce返回值 _debounce函数
const debounceChange = debounce(inputChange, 500, true, function () { })

// 通过零食函数从内部拿到debounce函数返回的Promise
const tempCallback = (...args) => {
  debounceChange.apply(inputEl, args).then(res => {
    console.log("Promise的返回值结果:", res)
  })
}

// oninput事件
inputEl.oninput = tempCallback

// 点击按钮取消事件
cancelBtn.onclick = function () {
  debounceChange.cancel()
}