JS防抖节流——电梯关门与地铁发车

类比

  • 防抖:电梯关门。最后一个人进入后,等待一段时间后才关门
  • 节流:地铁发车。每隔固定时间发一班车,不等人。

作用

间接影响 TTI(time to interactive),减少不必要的JS执行,释放主线程 => 加快页面响应。

防抖:通过将【一段时间内】,【高频且耗时的操作】合并为一次执行。

节流:【固定频率】执行操作,稀释执行密度。

应用场景

防抖:【搜索框用户输入联想】【窗口频繁的resize事件】

节流:【滚动事件监听】【动画渲染,保持 16ms 一帧的节奏】【用户滚动相关的一系列事件】

具体实现

防抖(debounce)

javascript 复制代码
export const debounce = (fn, delay) => {
  let timer = null

  return function (...args) {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(null, args)
    }, delay)
  }
}

防抖(立即执行)

增加一个第一次立即执行的标识即可

javascript 复制代码
export const debounceImmediate = (fn, delay) => {
  let timer = null
  let firstFlag = true
  return function (...args) {
    if (firstFlag) {
      fn.apply(this, args)
      firstFlag = false
      return
    }
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, delay)
  }
}

节流(throttle)date版本

因为时间比较符合直觉,一般用 date 实现。

javascript 复制代码
export const throttle = (fn, delay) => {
  let lastTime = 0
  return function (...args) {
    const now = +new Date()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}

节流(throttle)timer版本

javascript 复制代码
export const throttleTimer = (fn, delay) => {
  let timer = null
  return function (...args) {
    if (timer) {
      return
    }
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, delay)
  }
}

节流尾部调用版本

记录用户最后一次滚动常用此版本

除第一次外,都往加 timer的方向思考(因为除第一次,每一次都有可能是最后一次)

javascript 复制代码
export const throttleTrailing = (fn, delay, trailing = true) => {
  let lastTime = 0
  let timer = null
  return function (...args) {
    const curDate = +new Date()
    if (curDate - lastTime >= delay) {
      lastTime = curDate
      if (timer) {
        timer = null
        clearTimeout(timer)
      }
      fn.apply(this, args)
      return
    }
    if (!timer && trailing) {
      timer = setTimeout(
        () => {
          fn.apply(this, args)
          timer = null
        },
        delay - (curDate - lastTime),
      )
    }
  }
}

上述所有函数的测试代码

mocha+ chai

scss 复制代码
import {
  debounce,
  debounceImmediate,
  throttle,
  throttleTimer,
  throttleTrailing,
} from '../src/js/throttle-debounce.js'
import { expect } from 'chai'
import { describe } from 'mocha'

let count = 0
let trailingCount = 0

describe('throttle-debounce', () => {
  beforeEach(() => {
    count = 0
  })
  it('debounce normal', (done) => {
    const debounceFn = debounce(() => {
      ++count
    }, 100)
    debounceFn()
    debounceFn()
    debounceFn()
    setTimeout(() => {
      expect(count).to.equal(1)
      done()
    }, 500)
  })

  it('debounce immediate', (done) => {
    const debounceImmediateFn = debounceImmediate(() => {
      ++count
    }, 100)
    debounceImmediateFn()
    debounceImmediateFn()
    debounceImmediateFn()
    setTimeout(() => {
      expect(count).to.equal(2)
    }, 200)
    done()
  })

  it('throttle normal', (done) => {
    const throttleFn = throttle(() => {
      ++count
    }, 300)
    throttleFn() // 1
    setTimeout(() => {
      throttleFn()
      setTimeout(() => {
        throttleFn() // 2
        done()
      }, 300)
    }, 200)
  })

  it('throttle timer', (done) => {
    const throttleFn = throttleTimer(() => {
      ++count
    }, 300)
    throttleFn() // 1
    setTimeout(() => {
      throttleFn()
      setTimeout(() => {
        throttleFn() // 2
        done()
      }, 300)
    }, 200)
  })

  it('throttle trailing', (done) => {
    const throttleFn = throttleTrailing(() => {
      ++trailingCount
    }, 300)
    throttleFn() // 1
    setTimeout(() => {
      throttleFn() // 200 ms 之后又进行了执行,应该算是最后一次,理应执行
      setTimeout(() => {
        expect(trailingCount).to.equal(2)
        done()
      }, 500)
    }, 200)
  })
})
相关推荐
李姆斯18 小时前
为啥Agent在coding表现这么好,但是在别的领域就是差的不少?
前端·agent·ai编程
鱼与宇21 小时前
前端Web(html+css+js+vue3)
前端
雪芽蓝域zzs1 天前
(六)打包优化 + Nginx 部署完整配置 + 项目收尾
前端·vue.js
研☆香1 天前
聊一聊前端的常见字 关键字
开发语言·前端·javascript
东风破_1 天前
JWT 1:从一个登录请求开始,理解 React 项目里的 API 层与 Mock
前端·后端
东风破_1 天前
JWT 3:为什么 Token 要放进 Authorization?Axios 拦截器到底解决了什么?
前端·后端
东风破_1 天前
JWT 5:路由守卫是什么?把整个 JWT 登录鉴权流程串起来
前端·后端
东风破_1 天前
JWT 2:HTTP 是无状态的,为什么登录成功后还要给 Token?
前端·后端
东风破_1 天前
JWT 4:Zustand 到底解决了什么?为什么登录状态要放进 Store?
前端·后端
IT_陈寒1 天前
Vite动态导入差点让我秃头,原来问题出在这
前端·人工智能·后端