详细实现underScore同款功能throttle函数

282 阅读3分钟

写在最前面

在各大网络平台搜索手写防抖函数的文章或专栏,一些没有具体实现leading和trailing的功能,一些实现了但缺少详细的思维流程。

恰巧今天重温节流函数的手写,也当提醒自己写一篇文章来记录一下。

实现流程及功能

  1. throttle函数的基本实现(leading=true,trailing=false)

  2. leading = false, trailing = true ; leading = true, trailing = false 功能实现

  3. this绑定和参数的问题

  4. 取消功能的实现

  5. 返回值的问题

补充说明

Q:为什么没有实现leading和trailing都为false的情况?

A:如果都为false可能会出现:用户输入一次之后等待结果不继续输入,但是没有输出的情况

并且引用underscore封装的throttle函数发现也没有实现这个功能

throttle函数的基本实现

function throttle(fn, interval) {
  let lastTime = 0
  const _throttle = function () {
    const nowTime = new Date().getTime()
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      fn()
      lastTime = nowTime
    }
  }
  return _throttle
}

实现思路

  1. 真正执行时调用的是_throttle函数,所以throttle返回一个_throttle函数。

  2. 调用_throttle函数每次都要记录一下nowTime的值,将计算得到的remainTime与delay做比较,当remainTime的值小于等于delay时,意味着过了一个周期,此时执行一次fn函数。

  3. 将lastTime的值置为nowTime,再一次调用时会重新开始计算remainTime与delay做比较,相当于又是一次新的周期。

  4. 注意:当第一次点击时,由于lastTime值为0,而new Date().getTime()获取的一定是一个大数,所以会立即执行一次fn函数。

leading = false, trailing = true 和 leading = true, trailing = false 功能实现

function throttle(fn, interval, options = { leading: true, trailing: false }) {
  let lastTime = 0
  const { leading, trailing } = options
  let timer = null

  const _throttle = function () {
    const nowTime = new Date().getTime()

    // 处理leading
    if (!leading && !lastTime) {
      lastTime = nowTime
    }

    const remainTime = interval - (nowTime - lastTime)

    // fn函数执行
    if (remainTime <= 0) {
      // 当在remainTime正好为0时输入
      // 由于mainScript会先执行,所以可以取消计时器,回调函数不会执行
      // 但是如果不是正好为0,回调函数到时执行,fn函数也执行,一共会执行两次,出现问题
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      fn()
      lastTime = nowTime
      //如果不return会多加计时器
      return
    }

    // 处理trailing
    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        // 如果leading为false,lastTime = 0
        // 如果leading为true,lastTime = nowTime
        lastTime = !leading ? 0 : new Date().getTime()
        fn()
      }, remainTime)
    }
  }
  return _throttle
}

实现思路

  • leading为false时

    为了在最开始就等一个interval,那么将lastTime设置为nowTime。

    但如果单凭leading一个的值设置lastTime,那么每次调用_throttle都会将lastTime置为nowTime,一直等待interval。

    所以加入lastTime协同判断,当lastTime的值为0时,说明是第一次调用节流函数,将lastTime置为nowTime以等待interval

  • leading=false,trailing=true

    leading为false,若用户只输入一次,那么希望等到interval后也调用一次函数,所以可以考虑使用定时器:

    • 当trailing为false时,开启一个定时器,当用户在此期间不输入,到时间会自动执行setTimeout中的回调,其中会执行fn函数,并将timer设置回null,

      lastTime的值要根据leading来决定:

      当leading为false时,将lastTime置为0,这样当用户隔了很长一段时间不输入后,再次输入会回到 if (!leading && lastTime) {lastTime = nowTime}的判断,实现了leading为false的功能;

      当leading为true时,将lastTime设置为目前的时间,当用户隔了很长一段时间不输入,nowTime依旧远远大于lastTime,实现leading为true功能。

    • 当开启计时器后用户依然有输入:

      情况一: remainTime > 0

      此时应该继续之前计时,所以要有if (trailing && !timer)的判断

      情况二:remainTime = 0

      此时会执行if (remainTime <= 0)中的代码,即会执行fn函数,但是此时计时器也计时结束,由于setTimeout的回调函数是在宏队列中晚于mainScript中代码的执行,所以此时应该清除计时,将timer设置回null,最后return以防止再次开启定时器。

  • leading=true,trailing=true

    其实已经实现好了!在之前lastTime = !leading ? 0 : new Date().getTime()

this绑定和参数的问题

只要掌握this完全没问题

function throttle(fn, interval, options = { leading: true, trailing: false }) {
  let lastTime = 0
  let timer = null
  const { leading, trailing } = options

  const _throttle = function (...args) {
    const nowTime = new Date().getTime()
    if (!leading && !lastTime) {
      lastTime = nowTime
    }
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      fn.apply(this, args)
      lastTime = nowTime
      return
    }
    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        lastTime = !leading ? 0 : new Date().getTime()
        fn.apply(this, args)
      }, remainTime)
    }
  }
  return _throttle
}

取消功能的实现

function throttle(fn, interval, options = { leading: true, trailing: false }) {
  let lastTime = 0
  let timer = null
  const { leading, trailing } = options

  const _throttle = function (...args) {
    const nowTime = new Date().getTime()
    if (!leading && !lastTime) {
      lastTime = nowTime
    }
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      fn.apply(this, args)
      lastTime = nowTime
      return
    }
    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        lastTime = !leading ? 0 : new Date().getTime()
        fn.apply(this, args)
      }, remainTime)
    }
  }
  _throttle.cancel = function () {
    if(timer){
      clearTimeout(timer)
    }
    timer = null
    lastTime = 0
  }
  return _throttle
}

返回值的问题

不能直接return result的原因:

在其中执行result = fn.apply(this,args)时,result的赋值会在几秒之后,而mainScript已经执行结束,result依然为null

function throttle(fn, interval, options = { leading: true, trailing: false }) {
  let lastTime = 0
  let timer = null
  const { leading, trailing } = options

  const _throttle = function (...args) {
    return new Promise((resolve) => {
      const nowTime = new Date().getTime()
      // 处理leading
      if (!leading && !lastTime) {
        lastTime = nowTime
      }
      // 正常执行
      const remainTime = interval - (nowTime - lastTime)
      if (remainTime <= 0) {
        if (timer) {
          clearTimeout(timer)
          timer = null
        }
        const result = fn.apply(this, args)
        resolve(result)
        lastTime = nowTime
        return
      }
      // 处理trailing
      if (trailing && !timer) {
        timer = setTimeout(() => {
          timer = null
          const result = fn.apply(this, args)
          resolve(result)
          lastTime = !leading ? 0 : new Date().getTime()
        }, remainTime)
      }
    })
  }

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

  return _throttle
}

写在最后

这是我第一次写文章,如果有不妥希望能在评论区指出~