1、什么是节流

  • 在某个时间内(比如500ms),某个函数只能被触发一次;

过程:

  • 当事件触发时,相应的函数按照一定的频率去执行

2、节流的应用场景

  • 监听页面的滚动事件;

  • 鼠标移动事件;

  • 用户频繁点击按钮操作;

  • 游戏中的一些设计;

总之,依然是密集的事件触发,但是这次密集事件触发的过程,不会等待最后一次才进行函数调用,而是会按照一定的频率进行调用;

3、节流函数的实现

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

 3.1 节流基本功能

节流函数的默认实现思路我们采用时间戳的方式来完成:

  • 我们使用一个last来记录上一次执行的时间

    • 每次准备执行前,获取一下当前的时间now:now - last > interval

    • 那么函数执行,并且将now赋值给last即可

代码实现 :

//throttle.js
/**
 * @param {*} fn 要执行的函数
 * @param {*} interval 时间间隔
 * @returns 
 */
function throttle(fn, interval) {
  // 1.记录上一次的开始时间
  let lastTime = 0

  // 2.事件触发时, 真正执行的函数
  const _throttle = function () {

    // 2.1.获取当前事件触发时的时间
    const nowTime = new Date().getTime()

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长时间需要去触发函数
    const remainTime = interval - (nowTime - lastTime)
    //第一次会执行,原因是nowTime刚开始是一个很大的数字,结果为负数
    //若最后一次没能满足条件,不会执行
    if (remainTime <= 0) {
      // 2.3.真正触发函数
      fn()
      // 2.4.保留上次触发的时间
      lastTime = nowTime
    }
  }

  return _throttle
}

代码调用:

<!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="./throttle.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 = throttle(inputChange, 1000)
  </script>
</body>

</html>
  3.2 绑定this和参数

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

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

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

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

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

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

//throttle.js
/**
 * @param {*} fn 要执行的函数
 * @param {*} interval 时间间隔
 * @returns 
 */
function throttle(fn, interval) {
  // 1.记录上一次的开始时间
  let lastTime = 0

  // 2.事件触发时, 真正执行的函数
  const _throttle = function (...args) {

    // 2.1.获取当前事件触发时的时间
    const nowTime = new Date().getTime()

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长时间需要去触发函数
    const remainTime = interval - (nowTime - lastTime)
    //第一次会执行,原因是nowTime刚开始是一个很大的数字,结果为负数
    //若最后一次没能满足条件,不会执行
    if (remainTime <= 0) {
      // 2.3.真正触发函数
      fn.apply(this, args)
      // 2.4.保留上次触发的时间
      lastTime = nowTime
    }
  }

  return _throttle
}

代码调用:

<!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="./throttle.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 = throttle(inputChange, 1000)
  </script>
</body>

</html>
  3.3 节流函数第一次触发,优化立即执行

目前我们的函数触发第一次是都是立即执行的,但是某些场景是用户开始输入时的第一次是不立即执行的,我们可以如何优化呢?

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

  • leading为true时,或者不传(默认为true),第一次触发会立即执行

  • leading为false时,第一次不会立即执行

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

//throttle.js

/**
 * @param {*} fn 要执行的函数
 * @param {*} interval 时间间隔
 * @param {*} options 可选参数: leading第一次是否执行
 * @returns 
 */
function throttle(fn, interval, options = {leading: true }) {
  // 1.记录上一次的开始时间
  let lastTime = 0
  const {leading} = options

  // 2.事件触发时, 真正执行的函数
  const _throttle = function (...args) {

    // 2.1.获取当前事件触发时的时间
    const nowTime = new Date().getTime()
    //当lastTime为0时且第一次不执行,此时 remainTime = interval - (nowTime - lastTime) = interval > 0 
    if (!lastTime && !leading) lastTime = nowTime//决定第一次是否执行函数

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 2.3.真正触发函数
      fn.apply(this, args)
      // 2.4.保留上次触发的时间
      lastTime = nowTime
    }
  }

  return _throttle
}

代码调用:

<!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="./throttle.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 = throttle(inputChange, 2000, { leading: false })
  </script>
</body>

</html>
3.4 优化节流函数最后一次执行

默认情况下,我们的防抖函数最后一次是不会执行的

  • 因为没有达到最终的时间,也就是条件now - last > interval满足不了的

  • 但是,如果我们希望它最后一次是可以执行的,那么我们可以让其传入对应的参数来控制

我们来看一下代码如何实现:

  • 我们增加了判断语句:

    • 当最后一次需要执行且没有定时器时,只需要创建一个定时器,并且还有多久触发由remainTime决定

  • 如果固定的频率中执行了回调函数

    • 因为需要执行回调函数,所以如果存在定时器则需要取消;

    • 并且将timer赋值为null,这样的话可以开启下一次定时器;

  • 如果定时器最后执行了,那么timer需要赋值为null

    • 因为下一次重新开启时,只有定时器为null,才能进行下一次的定时操作;

//throttle.js

/**
 * @param {*} fn 要执行的函数
 * @param {*} interval 时间间隔
 * @param {*} options 可选参数: leading第一次是否执行, trailing 最后一次是否执行
 * @returns
 */
function throttle(fn, interval, options = {leading: true, trailing: false,}) {
  const { leading, trailing } = options;
  // 1.记录上一次的开始时间
  let lastTime = 0;
  let timer = null;

  // 2.事件触发时, 真正执行的函数
  const _throttle = function (...args) {
    // 2.1.获取当前事件触发时的时间
    const nowTime = new Date().getTime();
    //当lastTime为0时且第一次不执行
    if (!lastTime && !leading) lastTime = nowTime;

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
    const remainTime = interval - (nowTime - lastTime);
    if (remainTime <= 0) {
      if (timer) {
        //执行函数时,需要取消定时器
        clearTimeout(timer);
        timer = null;
      }

      // 2.3.真正触发函数
      fn.apply(this, args);
      // 2.4.保留上次触发的时间
      lastTime = nowTime;
      return;
    }

    //当最后一次需要执行且没有定时器时,只需要创建一个定时器,还有多久触发由remainTime决定
    if (trailing && !timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null; //执行后重置
        lastTime = !leading ? 0 : new Date().getTime(); //第一次不执行时为0,第一次执行时为定时器执行时的时间
      }, remainTime);
    }
  };
  return _throttle;
}

代码调用:

<!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="./throttle.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 = throttle(inputChange, 2000, { leading: true , trailing: true})
  </script>
</body>

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

与防抖函数的取消功能是类似的: 

//throttle.js
/**
 * @param {*} fn 要执行的函数
 * @param {*} interval 时间间隔
 * @param {*} options 可选参数: leading第一次是否执行, trailing 最后一次是否执行
 * @returns 
 */
function throttle(fn, interval, options = {leading: true,trailing: false}) {
  // 1.记录上一次的开始时间
  const {leading,trailing} = options
  let lastTime = 0
  let timer = null

  // 2.事件触发时, 真正执行的函数
  const _throttle = function (...args) {

    // 2.1.获取当前事件触发时的时间
    const nowTime = new Date().getTime()
    if (!lastTime && !leading) lastTime = nowTime

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }

      // 2.3.真正触发函数
      fn.apply(this, args)
      // 2.4.保留上次触发的时间
      lastTime = nowTime
      return
    }

    if (trailing && !timer) {
      timer = setTimeout(() => {
        fn.apply(this, args)
        timer = null
        lastTime = !leading ? 0 : new Date().getTime()
      }, remainTime)
    }
  }

  _throttle.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
    lastTime = 0
  }

  return _throttle
}

代码调用

<!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="./throttle.js"></script>
  <script>

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

    //输入触发事件
    const throttleChange = throttle(inputChange, 2000, { leading: false, trailing: true })
    inputEl.oninput = throttleChange

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

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

与防抖函数类似,有两种方法:

  • 方法一:通过回调函数

  • 方法二:通过Promise的resolve

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

//throttle.js

/**
 * @param {*} fn 要执行的函数
 * @param {*} interval 时间间隔
 * @param {*} options 可选参数: leading第一次是否执行, trailing 最后一次是否执行
 * @returns 
 */
function throttle(fn, interval, options = {leading: true,trailing: false}) {
  // 1.记录上一次的开始时间
  const { leading,trailing,resultCallback} = options
  let lastTime = 0
  let timer = null

  // 2.事件触发时, 真正执行的函数
  const _throttle = function (...args) {
      // 2.1.获取当前事件触发时的时间
      const nowTime = new Date().getTime()
      if (!lastTime && !leading) lastTime = nowTime

      // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
      const remainTime = interval - (nowTime - lastTime)
      if (remainTime <= 0) {
        if (timer) {
          clearTimeout(timer)
          timer = null
        }

        // 2.3.真正触发函数
        const result = fn.apply(this, args)
        if (resultCallback && typeof resultCallback === 'function') 
           resultCallback(result) //函数回调返回返回值
        // 2.4.保留上次触发的时间
        lastTime = nowTime
        return
      }

      if (trailing && !timer) {
        timer = setTimeout(() => {
          const result = fn.apply(this, args)
          if (resultCallback  && typeof resultCallback === 'function')  
           resultCallback(result) //函数回调返回返回值
          timer = null
          lastTime = !leading ? 0 : new Date().getTime()
        }, remainTime)
      }
  }

  _throttle.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
    lastTime = 0
  }

  return _throttle
}

代码调用:

<!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="./throttle.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 throttleChange = throttle(inputChange, 1000,{
      leading: false,
      trailing: true,
      resultCallback: function (res) {
        console.log("resultCallback:", res)
      }
    })
    inputEl.oninput = throttleChange

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

</html>

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

/**
 * @param {*} fn 要执行的函数
 * @param {*} interval 时间间隔
 * @param {*} options 可选参数: leading第一次是否执行, trailing 最后一次是否执行
 * @returns 
 */
function throttle(fn, interval, options = {leading: true,trailing: false}) {
  // 1.记录上一次的开始时间
  const { leading,trailing} = options
  let lastTime = 0
  let timer = null

  // 2.事件触发时, 真正执行的函数
  const _throttle = function (...args) {
    return new Promise((resolve, reject) => { //promise方式返回返回值
      // 2.1.获取当前事件触发时的时间
      const nowTime = new Date().getTime()
      if (!lastTime && !leading) lastTime = nowTime

      // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
      const remainTime = interval - (nowTime - lastTime)
      if (remainTime <= 0) {
        if (timer) {
          clearTimeout(timer)
          timer = null
        }

        // 2.3.真正触发函数
        const result = fn.apply(this, args)
        resolve(result)
        // 2.4.保留上次触发的时间
        lastTime = nowTime
        return
      }

      if (trailing && !timer) {
        timer = setTimeout(() => {
          const result = fn.apply(this, args)
          resolve(result)
          timer = null
          lastTime = !leading ? 0 : new Date().getTime()
        }, remainTime)
      }
    })
  }

  _throttle.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
    lastTime = 0
  }

  return _throttle
}

 代码调用:

<!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="./throttle.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 throttleChange = throttle(inputChange, 3000, {
      leading: false,
      trailing: true
    })
    const tempCallback = function (...args) {
      throttleChange.apply(this, args).then(res => {//此时this绑定的是input对象
        console.log("Promise的返回值结果:", res)
      })
    }
    inputEl.oninput = tempCallback

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

</html>
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐