2021-08-31 前端每日一题——防抖和节流

165 阅读2分钟

前端每日一题,今天开始会每天更新,放置于:upupming/frontend-learning-map: 前端学习路线(TODOs) (github.com)

  • 防抖和节流的区别是,防抖一上来就是先等待执行,而节流是先执行了再等待,从代码上可以看到区别,具体适用场景的话,
    • debounce 常用于窗口 resize 和键盘输入,当稳定之后再执行逻辑能够减少资源消耗
    • 如果用户滑到底部发送 API 请求,显然用 throttle 合适,因为 debounce 会对请求进行延迟
// https://github.com/haizlin/fe-interview/issues/1043
/**
 * 防抖:生成一个函数,它在被调用后会等待一段时间再执行
 * 如果在等待期间再次调用,之前还未执行的调用会被取消
 * 参考: https://lodash.com/docs/4.17.15#debounce
 * @param fn 要防抖的函数
 * @param timeout 超时时间
 */
function debounce<F extends (...args: any[]) => void> (fn: F, timeout: number) {
  let timerId: NodeJS.Timeout | null = null
  return function debounced (...args: Parameters<F>): void {
    // 两次间隔太短的调用会导致上一次直接被 clearTimeout 掉
    if (timerId) {
      clearTimeout(timerId)
    }
    timerId = setTimeout(() => {
      fn(...args)
      timerId = null
    }, timeout)
  }
}

describe('debounce', () => {
  it('should execute only once in timeout', async () => {
    let [accumulator, times, timeout] = [0, 100, 50]
    const add = jest.fn((a: number) => { accumulator += a })
    const addDebounced = debounce(add, timeout)
    for (let i = 0; i < times; i++) {
      addDebounced(1)
      // 由于是延时执行,这里一定没有执行,必须后面等待完毕
      expect(add).toHaveBeenCalledTimes(0)
    }
    await new Promise((resolve) => setTimeout(resolve, 100))
    expect(add).toHaveBeenCalledTimes(1)
    expect(accumulator).toEqual(1)
    addDebounced(1); addDebounced(1)
    await new Promise((resolve) => setTimeout(resolve, 100))
    expect(add).toHaveBeenCalledTimes(2)
    expect(accumulator).toEqual(2)
  })
})

/**
 * 节流:生成一个函数,它在被调用一段时间内再被调用直接忽视
 * @param fn 要节流的函数
 * @param timeout 超时时间
 */
function throttle<F extends (...args: any[]) => void> (fn: F, timeout: number) {
  let timerId: NodeJS.Timeout | null = null
  return function throttled (...args: any[]) {
    if (timerId === null) {
      fn(...args)
      timerId = setTimeout(() => {
        timerId = null
      }, timeout)
    }
  }
}
/**
 * 不需要 timer 也可以实现 throttle
 */
function throttleNoTimer<F extends (...args: any[]) => void> (fn: F, timeout: number) {
  let lastTime: number|null = null
  return function throttled (...args: any[]) {
    const nowTime = Date.now()
    if (!lastTime || nowTime - lastTime > timeout) {
      fn(...args)
      lastTime = nowTime
    }
  }
}

describe('throttle', () => {
  it('should execute only once in timeout', async () => {
    let [accumulator, times, timeout] = [0, 100, 50]
    const add = jest.fn((a: number) => { accumulator += a })
    const addThrottled = throttle(add, timeout)
    for (let i = 0; i < times; i++) {
      addThrottled(1)
      // 由于是立即执行,这里一定至少已经执行了依次
      expect(add).toHaveBeenCalledTimes(1)
    }
    await new Promise((resolve) => setTimeout(resolve, 100))
    expect(add).toHaveBeenCalledTimes(1)
    expect(accumulator).toEqual(1)
    addThrottled(1); addThrottled(1)
    await new Promise((resolve) => setTimeout(resolve, 100))
    expect(add).toHaveBeenCalledTimes(2)
    expect(accumulator).toEqual(2)
  })
})