1、什么是防抖

在某设定的时间内,没有再次触发某个函数时,才真正的调用这个函数。

过程:

  • 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;

  • 当事件密集触发时,函数的触发会被频繁的推迟;

  • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;

2、防抖的应用场景

  • 输入框中频繁的输入内容,搜索或者提交信息;

  • 频繁的点击按钮,触发某个事件;

  • 监听浏览器滚动事件,完成某些特定操作;

  • 用户缩放浏览器的resize事件;

总之,密集的事件触发,我们只希望触发比较靠后发生的事件,就可以使用防抖函数;

3、防抖函数的实现

下面我们根据需求场景,一步一步深入实现防抖函数

 3.1 防抖基本功能

防抖函数的核心思路如下:

  • 当触发一个函数时,并不会立即执行这个函数,而是会延迟(通过定时器来延迟函数的执行)

    • 如果在延迟时间内,有重新触发函数,那么取消上一次的函数执行(取消定时器);

    • 如果在延迟时间内,没有重新触发函数,那么这个函数就正常执行(执行传入的函数);

接下来,就是将思路转成代码即可:

  • 定义debounce函数要求传入两个参数

    • 需要处理的函数fn;

    • 延迟时间;

  • 通过定时器来延迟传入函数fn的执行

    • 如果在此期间有再次触发这个函数,那么clearTimeout取消这个定时器;

    • 如果没有触发,那么在定时器的回调函数中执行即可;

代码实现 :

//debounce.js

/**
 * @param {*} fn 要执行的函数
 * @param {*} delay 延迟时间
 * @returns 
 */
function debounce(fn, delay) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null

  // 2.真正执行的函数
  const _debounce = function () {
    // 取消上一次的定时器
    if (timer) clearTimeout(timer)
    // 延迟执行
    timer = setTimeout(() => {
      // 外部传入的真正要执行的函数
      fn()
    }, delay)
  }

  return _debounce
}

代码调用:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <input type="text">
  <button id="cancel">取消</button>
  <script src="./debounce.js"></script>
  <script>

    const inputEl = document.querySelector("input")
    const cancelBtn = document.querySelector("#cancel")
    let counter = 0

    //输入触发事件
    const inputChange = function (event) {
      console.log(`发送了第${++counter}次网络请求`)
    }
    inputEl.oninput = debounce(inputChange, 1000)
  </script>
</body>

</html>
  3.2 绑定this和参数

  我们知道在oninput事件触发时会有参数传递,并且触发的函数中this是指向当前的元素节点的

  • 目前我们fn的执行是一个独立函数调用,它里面的this是window

    • 我们需要将其修改为对应的节点对象,而返回的function中的this指向的是节点对象;

  • 目前我们的fn在执行时是没有传递任何的参数的,它需要将触发事件时传递的参数传递给fn

    • 而我们返回的function中的arguments正是我们需要的参数;

所以我们的代码可以进行如下的优化:

//debounce.js

/**
 * @param {*} fn 要执行的函数
 * @param {*} delay 延迟时间
 * @returns 
 */
function debounce(fn, delay) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null

  // 2.真正执行的函数
  const _debounce = function (...args) {
    // 取消上一次的定时器
    if (timer) clearTimeout(timer)
    // 延迟执行
    timer = setTimeout(() => {
      // 外部传入的真正要执行的函数,绑定this和参数
      fn.apply(this, args)
    }, delay)
  }

  return _debounce
}

代码调用:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <input type="text">
  <button id="cancel">取消</button>
  <script src="./debounce.js"></script>
  <script>

    const inputEl = document.querySelector("input")
    const cancelBtn = document.querySelector("#cancel")
    let counter = 0

    //输入触发事件
    const inputChange = function (event) {
      console.log(`发送了第${++counter}次网络请求`, this, event)
    }
    inputEl.oninput = debounce(inputChange, 1000)
  </script>
</body>

</html>
  3.3 防抖函数第一次触发,立即执行

目前我们的事件触发都要等到delay时间,但是某些场景是用户开始输入时的第一次是立即执行的,后续的输入才需要等待,我们可以如何优化呢?

  • 我们可以让用户多传入一个参数:immediate

    • 那么第一次就立即执行

    • 后来的事件需要等待delay时间执行

    • immediate为false,或者不传,那么按照上面的防抖进行操作

    • immediate为true

  • 我们可以根据是否传入immediate进行不同的处理方式:

//debounce.js

/**
 * @param {*} fn 要执行的函数
 * @param {*} delay 延迟时间
 * @param {*} immediate 是否立即执行
 * @returns 
 */
function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null;
  let isInvoke = false; //记录立即执行是否已执行过

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

    // 判断是否需要立即执行
    if (immediate && !isInvoke) {
      fn.apply(this, args);
      isInvoke = true;
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        // 外部传入的真正要执行的函数
        fn.apply(this, args);

        //没有这个步骤时,只有第一次输入是立即执行,即使后面延迟执行后再输入也是延迟执行;
        // 有这个步骤时,第一次输入时立即执行,后面延迟执行后再输入也会有立即执行
        isInvoke = false
        timer = null
      }, delay);
    }
  };

  return _debounce;
}

代码调用

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <input type="text">
  <button id="cancel">取消</button>
  <script src="./debounce.js"></script>
  <script>

    const inputEl = document.querySelector("input")
    const cancelBtn = document.querySelector("#cancel")
    let counter = 0

    //输入触发事件
    const inputChange = function (event) {
      console.log(`发送了第${++counter}次网络请求`, this, event)
    }
    inputEl.oninput = debounce(inputChange, 1000, true)
  </script>
</body>

</html>
  3.4 当我们触发函数时,在未到执行时间,可以取消函数执行 

有时候,在等待执行的过程中,可能需要取消之前的操作:

  • 比如用户进行了搜索,但是还没有来得及发送搜索的情况下,退出了界面;

  • 当用户退出时,之前的操作就可以取消掉;

我们这里将delay时间改长,并且在下方增加一个按钮:

  • 在延迟时间内,我们点击按钮,就取消之前的函数执行: 

//debounce.js

/**
 * @param {*} fn 要执行的函数
 * @param {*} delay 延迟时间
 * @param {*} immediate 是否立即执行
 * @returns 
 */
function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null;
  let isInvoke = false; //记录立即执行是否已执行过

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

    // 判断是否需要立即执行
    if (immediate && !isInvoke) {
      fn.apply(this, args);
      isInvoke = true;
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        // 外部传入的真正要执行的函数
        fn.apply(this, args);

        //没有这个步骤时,只有第一次输入是立即执行,即使后面延迟执行后再输入也是延迟执行;
        // 有这个步骤时,第一次输入时立即执行,后面延迟执行后再输入也会有立即执行
        isInvoke = false
        timer = null
      }, delay);
    }
  };

// 封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null //重置
    isInvoke = false //重置
  }

  return _debounce;
}

代码调用

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <input type="text">
  <button id="cancel">取消</button>
  <script src="./debounce.js"></script>
  <script>

    const inputEl = document.querySelector("input")
    const cancelBtn = document.querySelector("#cancel")
    let counter = 0

    //输入触发事件
    const debounceChange = debounce(inputChange, 2000, true)
    inputEl.oninput = debounceChange

    // 取消功能
    cancelBtn.onclick = function () {
      debounceChange.cancel()
    }
  </script>
</body>

</html>
 3.5 当触发的函数有返回值时,获取在防抖函数中执行的结果

有时候fn函数执行结束后还有返回值,如果我们希望拿到这个返回值应该怎么办呢?

先明确一个操作:

  • 内部执行fn函数大多数情况是异步执行的(在setTimeout中执行)

  • 所以通过return是无法拿到返回值的

异步的操作如何获取返回值呢?

  • 方法一:通过回调函数

  • 方法二:通过Promise的resolve

 (1)给debounce函数,多添加一个参数,参数为一个回调函数:

//debounce.js

/**
 * @param {*} fn 要执行的函数
 * @param {*} delay 延迟时间
 * @param {*} immediate 是否立即执行
 * @param {*} resultCallback 用来操作返回值的函数
 * @returns 
 */
function debounce(fn, delay, immediate = false, resultCallback) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null;
  let isInvoke = false; //记录立即执行是否已执行过

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

    // 判断是否需要立即执行
    if (immediate && !isInvoke) {
      const result = fn.apply(this, args)
      if (resultCallback) resultCallback(result) //通过函数参数来返回值
      isInvoke = true;
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        // 外部传入的真正要执行的函数
        const result = fn.apply(this, args)
        if (resultCallback) resultCallback(result)

        //没有这个步骤时,只有第一次输入是立即执行,即使后面延迟执行后再输入也是延迟执行;
        // 有这个步骤时,第一次输入时立即执行,后面延迟执行后再输入也会有立即执行
        isInvoke = false
        timer = null
      }, delay);
    }
  };

// 封装取消功能
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null //重置
    isInvoke = false //重置
  }

  return _debounce;
}

代码调用:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <input type="text">
  <button id="cancel">取消</button>
  <script src="./debounce.js"></script>
  <script>

    const inputEl = document.querySelector("input")
    const cancelBtn = document.querySelector("#cancel")
    let counter = 0

    const inputChange = function (event) {
      console.log(`发送了第${++counter}次网络请求`, this, event)
      // 返回值
      return "aaaaaaaaaaaa"
    }

    // 方法一 通过添加一个函数参数来获取
    const debounceChange = debounce(inputChange, 1000, true, (res) => {
      console.log("函数参数返回的值:", res)
    })
    inputEl.oninput = debounceChange

    // 取消功能
    cancelBtn.onclick = function () {
      debounceChange.cancel()
    }
  </script>
</body>

</html>

 (2)使用Promise来返回执行结果:

/**
 * @param {*} fn 要执行的函数
 * @param {*} delay 延迟时间
 * @param {*} immediate 是否立即执行
 * @returns 
 */
function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  let isInvoke = false

  // 2.真正执行的函数
  const _debounce = function (...args) {
    return new Promise((resolve, reject) => { //通过promise来返回值
      // 取消上一次的定时器
      if (timer) clearTimeout(timer)

      // 判断是否需要立即执行
      if (immediate && !isInvoke) {
        const result = fn.apply(this, args)
        resolve(result)
        isInvoke = true
      } else {
        // 延迟执行
        timer = setTimeout(() => {
          // 外部传入的真正要执行的函数
          const result = fn.apply(this, args)
          resolve(result)
          isInvoke = false
          timer = null
        }, delay)
      }
    })
  }

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

  return _debounce
}

 代码调用:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <input type="text">
  <button id="cancel">取消</button>
  <script src="./debounce.js"></script>
  <script>

    const inputEl = document.querySelector("input")
    const cancelBtn = document.querySelector("#cancel")
    let counter = 0

    const inputChange = function (event) {
      console.log(`发送了第${++counter}次网络请求`, this, event)
      // 返回值
      return "aaaaaaaaaaaa"
    }

    // 方法二 返回一个promise
    const debounceChange = debounce(inputChange, 3000, false)//相当于_debounce
    const tempCallback = function (...args) {
      debounceChange.apply(this, args).then(res => {//此时this绑定的是input对象
        console.log("Promise的返回值结果:", res)
      })
    }
    inputEl.oninput = tempCallback

    // 取消功能
    cancelBtn.onclick = function () {
      debounceChange.cancel()
    }
  </script>
</body>

</html>
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐