# 认识节流 throttle 函数

  • 我们来理解一下节流的过程

    • 当事件触发时,会执行这个事件的响应函数

    • 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数

    • 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的

  • 节流的应用场景:

    • 监听页面的滚动事件
    • 鼠标移动事件
    • 用户频繁点击按钮操作
    • 游戏中的一些设计

# 节流函数的应用场景

  • 很多人都玩过类似于 王者荣耀或者LOL

    • 当玩 ADC 时,哪怕你疯狂点平 A,射手的射速与你点击的频率没有太大关系
    • 因为游戏中它们是由自己的 攻速阈值 的,那么这种攻速阈值带来的效果就是 节流
  • 很多人也都玩过类似于飞机大战的游戏

  • 在飞机大战的游戏中,我们按下空格会发射一个子弹:

    • 很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射
    • 比如 1 秒钟只能发射一次,即使用户在这 1 秒钟按下了 10 次,子弹会保持发射一颗的频率来发射
    • 但是事件是触发了 10 次的,响应的函数只触发了一次

# 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">
  • js 代码测试
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function (event) {
  console.log(`发送了第${++counter}次网络请求`, this, event)
}
inputEl.oninput = _.throttle(inputChange, 1000)

# throttle v1 基本实现

  • 这里实现节流函数思路是采用 时间戳 的方式
    • 采用 lastTime 来记录每次执行的上一次函数触发的开始时间,默认为 0
    • 然后通过 传入的时间间隔与当前触发的时间以及上一次触发的开始的时间, 计算出还剩余多长事件需要去触发函数
    • 最后触发函数时,会将 当前触发的时间赋值给用来保存上次触发事件的变量 ,实现 节流 效果
function throttle(fn, interval) {
  // 1. 记录上一次的开始时间
  let lastTime = 0
  // 2. 事件触发时,执行的函数
  const _throttle = function () {
    // 2.1 获取当前事件触发时的时间
    const nowTime = +new Date()
    // 2.2 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 2.3 真正触发的函数
      fn()
      // 2.4 保留上次触发的事件
      lastTime = nowTime
    }
  }
  return _throttle
}
  • 那么这就是 节流函数 的基本实现,接下来还会增加一些功能

# throttle v2 leading 实现

  • 实现思路就是传入一个对象,对象控制两个变量 leadingtrailing
    • leading 控制第一次是否执行
    • trailing 控制最后一次是否执行
  • 这里先实现 leading 功能,那么 leading 这里的默认值我设置了 true ,也就是默认第一次就执行
  • 就下了就涉及到一个小算法,就是当 leadingtrue 时,那么将 nowTime 赋值给 lastTime
    • 就比如当前触发开始时间 (nowTime) 是 1000000 , 那么将上一次触发开始时间 (lastTime) 等于 1000000
    • 在执行 nowTime - lastTime 时,结果必然为 0 , 那么就满足下面的 if 判断了,就会执行函数了!
  • 但还要一点要注意,立即执行的前提必然是每次节流函数的第一次生效,所以还需判断 lastTime 是否为 0 ,当 lastTime === 0 代表是第一次执行,才会进行上面 leading 的判断
function throttle(fn, interval, options = { leading: true, trailing: false}) {
  // 1.1 控制第一次与最后一次是否执行
  const { leading, trailing } = options
  // 1.2 记录上一次的开始时间
  let lastTime = 0
  // 2. 事件触发时,执行的函数
  const _throttle = function () {
    // 2.1 获取当前事件触发时的时间
    const nowTime = +new Date()
    // 2.2 如果 lastTime 为 0 并且 leading 为 false  将 nowTime 赋值给 lastTime
    if(!lastTime && !leading) lastTime = nowTime
    // 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还剩余多长事件需要去触发函数
    // 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 2.4 真正触发的函数
      fn()
      // 2.5 保留上次触发事件的时间戳
      lastTime = nowTime
    }
  }
  return _throttle
}
  • 这里先进行代码测试
  • HTML
<input type="text">
  • JS代码
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function (event) {
  console.log(`发送了第${++counter}次网络请求`, this, event);
}
//leading 这里默认就是 true,所以我们测试 false,那么第一次不会即立即触发执
inputEl.oninput = throttle(inputChange, 1000, { leading: false })

# throttle v3 traling 实现

  • 这里 traling 默认为 false ,也就是默认最后一次不会执行

  • 接下来判断如果 trailingtrue 并且没有执行过定时器时:

    • 开启定时器并将定时器编号给 timer,方便取消定时器
    • 执行定时器时,再将 timer 初始化
    • 判断 leadingtrue 时, lastTime 就需要重新获取时间
  • 比如 interval 是为 10s 时

    • 触发事件也是第 10s 时,就会执行 if (remainTime <= 0) 里面的代码,然后会终止执行
  • 触发事件的时间大概率不会刚好与 interval 相等,后面很有可能会有 ms 的,就比如触发事件是 10.1s(10100ms)

    • 首先 10 - (10.1 - 0) 是小于 0 的,那么 if (remainTime <= 0) 里面的代码依旧会执行
    • 但此时 remainTime 是为 -0.1s 的,所以还会根据 trailing :true 进入函数,再进行最后一次的回调
    • 那么也就是说,会重复执行两次函数
  • 那怎么解决呢?

    • 就是进行一个判断,当 trailingtrue
    • lastTime 就通过 new Date() 重新获取下当前的时间戳并赋值,意味着用 interval - (nowTime - lastTime) 重新求出 remain 值作为 setTimeout 延迟时间
    • 否则的话就初始化变量为 0
function throttle(fn, interval, options = { leading: true, trailing: false}) {
  // 1.1 控制第一次与最好一次是否执行
  let { leading, trailing } = options
  // 1.2 记录上一次的开始时间
  let lastTime = 0
  // 1.3 用于判断是否存在定时器
  let timer = null
  // 2. 事件触发时,执行的函数
  const _throttle = function () {
    // 2.1 获取当前事件触发时的时间
    const nowTime = +new Date()
    
    // 2.2 如果 lastTime 为 0 并且 leading 为 false  将 nowTime 赋值给 lastTime
    if (!lastTime && !leading) lastTime = nowTime
    // 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还剩余多长事件需要去触发函数
    // 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 3.1 如果有定时器就取消定时器并初始化 timer
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      // 2.4 真正触发的函数
      fn()
      // 2.5 保留上次触发的事件 并终止代码不执行定时器
      return lastTime = nowTime
    }
    // 3. 如果 trailing 为 true 并且 没有定时器执行下面代码
    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        // 3.2 需要重新获取时间
        lastTime = +new Date()
          
        fn()
      }, remainTime)
    }
  }
  return _throttle
}
  • 这里先进行代码测试
  • HTML
<input type="text">
  • JS代码
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function (event) {
  console.log(`发送了第${++counter}次网络请求`, this, event);
}
// 这里将 trailing 设为 true 最后一次会调用
inputEl.oninput = throttle(inputChange, 1000, { leading: false, trailing: true })

# throttle v4 this 参数

  • this 的写法与 防抖 写法一样,可以用 apply 或 call 都可以
  • bind不行吗?可以的,在后面加括号调用也可 ,这里我依旧采用 apply
function throttle(fn, interval, options = { leading: true, trailing: false}) {
  // 1.1 控制第一次与最好一次是否执行
  let { leading, trailing } = options
  // 1.2 记录上一次的开始时间
  let lastTime = 0
  // 1.3 用于判断是否存在定时器
  let timer = null
  // 2. 事件触发时,执行的函数
  const _throttle = function (...args) {
    // 2.1 获取当前事件触发时的时间
    const nowTime = new Date().getTime()
    
    // 2.2 如果 lastTime 为 0 并且 leading 为 false  将 nowTime 赋值给 lastTime
    if (!lastTime && !leading) lastTime = nowTime
    // 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还剩余多长事件需要去触发函数
    // 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
    const remainTime = interval - (nowTime - lastTime)
    
    if (remainTime <= 0) {
      // 3.1 如果有定时器就取消定时器并初始化 timer
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      // 2.4 真正触发的函数 并传入 this 与 参数
      fn.apply(this, args)
      // 2.5 保留上次触发的事件 并终止代码不执行定时器
      return lastTime = nowTime
    }
    // 3. 如果 trailing 为 true 并且 没有定时器执行下面代码
    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        // 3.2 需要重新获取时间
        lastTime = +new Date()
        // 3.3 传入 this 与 参数
        fn.apply(this, args)
      }, remainTime)
    }
  }
  return _throttle
}
  • 测试代码用上面的就行

# throttle v5 取消功能

  • 这也是比较简单的,取消定时器就可以了,那么可能会有疑问了,如果 tralingfalse 怎么要取消呢?
  • 其实这里针对 traling 功能就好了,因为如果在 tralingfalse 的情况下,你输入的时间小于节流函数的 interval 本质上也不会执行
  • 所以针对 tralingtrue 时,进行取消是没有问题的
function throttle(fn, interval, options = { leading: true, trailing: false}) {
  // 1.1 控制第一次与最好一次是否执行
  let { leading, trailing } = options
  // 1.2 记录上一次的开始时间
  let lastTime = 0
  // 1.3 用于判断是否存在定时器
  let timer = null
  // 2. 事件触发时,执行的函数
  const _throttle = function (...args) {
    // 2.1 获取当前事件触发时的时间
    const nowTime = new Date().getTime()
    
    // 2.2 如果 lastTime 为 0 并且 leading 为 false  将 nowTime 赋值给 lastTime
    if (!lastTime && !leading) lastTime = nowTime
    // 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还剩余多长事件需要去触发函数
    // 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
    const remainTime = interval - (nowTime - lastTime)
    
    if (remainTime <= 0) {
      // 3.1 如果有定时器就取消定时器并初始化 timer
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      // 2.4 真正触发的函数 并传入 this 与 参数
      fn.apply(this, args)
      // 2.5 保留上次触发的事件 并终止代码不执行定时器
      return lastTime = nowTime
    }
    // 3. 如果 trailing 为 true 并且 没有定时器执行下面代码
    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        // 3.2 需要重新获取时间
        lastTime = +new Date()
        // 3.3 传入 this 与 参数
        fn.apply(this, args)
      }, remainTime)
    }
  }
  // 4. 取消功能
  _throttle.cancel = function () {
    if (timer) clearTimeout(timer)
    // 4.1 取消代表整个函数终结了,那么建议初始化变量
    timer = null
    lastTime = 0
  }
  return _throttle
}
  • 这里也可以进行代码测试
  • HTML
<input type="text">
<button id="cancel">取消</button>
  • JS代码
const inputEl = document.querySelector("input")
const btnEl = document.querySelector("button")
let counter = 0
const inputChange = function (event) {
  console.log(`发送了第${++counter}次网络请求`, this, event);
}
// 拿到 throttle 返回值
const _throttle = throttle(inputChange, 1000, { leading: false, trailing: true })
// 这种写法与之前的写法没有区别 都是调用 _throttle
inputEl.oninput = _throttle
// 点击按钮取消
btnEl.onclick = () => _throttle.cancel()

# throttle v6 函数返回值

# 思路一:回调函数

  • 通过外界传入函数回调形式返回返回值
function throttle(fn, interval, options = { leading: true, trailing: false}) {
  // 1.1 控制第一次与最好一次是否执行
  let { leading, trailing, resultCallback } = options
  // 1.2 记录上一次的开始时间
  let lastTime = 0
  // 1.3 用于判断是否存在定时器
  let timer = null
  // 2. 事件触发时,执行的函数
  const _throttle = function (...args) {
    // 2.1 获取当前事件触发时的时间
    const nowTime = new Date().getTime()
    
    // 2.2 如果 lastTime 为 0 并且 leading 为 false  将 nowTime 赋值给 lastTime
    if (!lastTime && !leading) lastTime = nowTime
    // 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还剩余多长事件需要去触发函数
    // 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
    const remainTime = interval - (nowTime - lastTime)
    
    if (remainTime <= 0) {
      // 3.1 如果有定时器就取消定时器并初始化 timer
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      // 2.4 真正触发的函数 并传入 this 与 参数  再拿到其返回值
      const result = fn.apply(this, args)
      // 2.5 判断 resultCallback 是否传入并是否为函数 再将返回值传入回调函数
      if(resultCallback && typeof resultCallback === 'function') resultCallback(result)
      // 2.6 保留上次触发的事件 并终止代码不执行定时器
      return lastTime = nowTime
    }
    // 3. 如果 trailing 为 true 并且 没有定时器执行下面代码
    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        // 3.2 需要重新获取时间
        lastTime = +new Date()
        // 3.3 传入 this 与 参数  并拿到其返回值
        const result = fn.apply(this, args)
        // 3.4 判断 resultCallback 是否传入并是否为函数 再将返回值传入回调函数
        if(resultCallback && typeof resultCallback === 'function') resultCallback(result)
      }, remainTime)
    }
  }
  // 4. 取消功能
  _throttle.cancel = function () {
    if (timer) clearTimeout(timer)
    // 4.1 取消代表整个函数终结了,那么建议初始化变量
    timer = null
    lastTime = 0
  }
  return _throttle
}

# 思路二:Promise

  • 通过返回 Promise 外部进行 then 方法调用来获取返回值
function throttle(fn, interval, options = { leading: true, trailing: false}) {
  // 1.1 控制第一次与最好一次是否执行
  let { leading, trailing, resultCallback } = options
  // 1.2 记录上一次的开始时间
  let lastTime = 0
  // 1.3 用于判断是否存在定时器
  let timer = null
  // 2. 事件触发时,执行的函数
  const _throttle = function (...args) {
    return new Promise((resolve, reject) => {
      // 2.1 获取当前事件触发时的时间
    const nowTime = new Date().getTime()
    
    // 2.2 如果 lastTime 为 0 并且 leading 为 false  将 nowTime 赋值给 lastTime
    if (!lastTime && !leading) lastTime = nowTime
    // 2.3 使用当前触发的时间和之前的时间间隔以及上一次开始的时间,计算出还剩余多长事件需要去触发函数
    // 余下时间 = 间隔时间 - (触发开始时间 - 上一次的触发开始时间)
    const remainTime = interval - (nowTime - lastTime)
    
    if (remainTime <= 0) {
      // 3.1 如果有定时器就取消定时器并初始化 timer
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      // 2.4 真正触发的函数 并传入 this 与 参数  再拿到其返回值
      const result = fn.apply(this, args)
      // 2.5 判断 resultCallback 是否传入并是否为函数 再将返回值传入回调函数
      try {
        if(resultCallback && typeof resultCallback === 'function') resolve(result)
      } catch (err) {
        reject(err)
      }
      // 2.6 保留上次触发的事件 并终止代码不执行定时器
      return lastTime = nowTime
    }
    // 3. 如果 trailing 为 true 并且 没有定时器执行下面代码
    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        // 3.2 需要重新获取时间
        lastTime = +new Date()
        // 3.3 传入 this 与 参数  并拿到其返回值
        const result = fn.apply(this, args)
        // 3.4 判断 resultCallback 是否传入并是否为函数 再将返回值传入回调函数
        try {
          if(resultCallback && typeof resultCallback === 'function') resolve(result)
        } catch (err) {
          reject(err)
        }
      }, remainTime)
    }
    })
  }
  // 4. 取消功能
  _throttle.cancel = function () {
    if (timer) clearTimeout(timer)
    // 4.1 取消代表整个函数终结了,那么建议初始化变量
    timer = null
    lastTime = 0
  }
  return _throttle
}
  • 这里我依旧使用代码对 Promise实现的返回值功能 进行简单测试:
  • HTML测试代码
<input type="text">
<button id="cancel">取消</button>
  • js测试代码
const inputEl = document.querySelector("input")
const btnEl = document.querySelector("button")
let counter = 0
const inputChange = function (event) {
  console.log(`发送了第${++counter}次网络请求`, this, event)
    
  // 返回值:返回 0-99 随机一个 s
  return ~~(Math.random() * 100)
}
// 拿到 throttle 返回值
const _throttle = throttle(inputChange, 1000, {
  leading: false,
  trailing: false,
  resultCallback() { }
})
// 通过临时函数获得_throttle 返回值 -> Promsie 通过 then 方法拿到返回值
const tempCallback = function () {
  const res = _throttle().then(res => {
    console.log("Promise的返回值结果:", res)
  })
}
// 调用 tempCallback 函数这种写法与之前的写法没有区别 也都是调用 _throttle
inputEl.oninput = tempCallback
// 点击按钮取消
btnEl.onclick = () => _throttle.cancel()