# 认识防抖 debounce 函数

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

# 防抖函数的案例

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

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

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

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

  • 比如想要搜索 nekoaimer

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

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

# Underscore 库的介绍

  • 事实上我们可以通过一些第三方库来实现防抖操作:
    • lodash
    • underscore
  • 这里使用 underscore
    • 我们可以理解成 lodash 是 underscore 的升级版,它更重量级,功能也更多;
    • 但是目前我看到 underscore 还在维护,lodash 已经很久没有更新了;
  • Underscore 的官网: https://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()
}