← Собеседование/Реализуй debounce и throttle#374 из 383← ПредыдущийСледующий →+40 XP
Полезно по теме:Маршрут: подготовка к интервьюГайд: портфолио juniorГайд: карьерный планТермин: Closure
← НазадДалее →

Реализуй debounce и throttle

Краткий ответ

Debounce откладывает выполнение функции до тех пор, пока не пройдёт N миллисекунд после последнего вызова — идеален для поиска и resize. Throttle гарантирует, что функция вызывается не чаще одного раза за N миллисекунд — идеален для scroll и кликов. Оба паттерна реализуются через замыкания и таймеры.

Полный разбор

Debounce — «подождать тишины»

Представь строку поиска: пользователь печатает «iPhone 15 Pro». Без debounce будет 13 запросов к API. С debounce — только один, когда пользователь остановился.

function debounce(fn, delay) {
  let timerId = null

  return function(...args) {
    // Отменяем предыдущий таймер при каждом вызове
    clearTimeout(timerId)

    // Запускаем новый — выполнится только если не будет новых вызовов
    timerId = setTimeout(() => {
      fn.apply(this, args)
      timerId = null
    }, delay)
  }
}

// Использование
const search = debounce((query) => {
  console.log('Запрос к API:', query)
}, 300)

search('i')       // сброс, новый таймер
search('ip')      // сброс, новый таймер
search('iph')     // сброс, новый таймер
// через 300ms: 'Запрос к API: iph'

Throttle — «не чаще раза в N ms»

Scroll-handler срабатывает 100+ раз в секунду. Throttle ограничивает до 1 раза в 100ms.

function throttle(fn, interval) {
  let lastCallTime = 0

  return function(...args) {
    const now = Date.now()

    if (now - lastCallTime >= interval) {
      lastCallTime = now
      fn.apply(this, args)
    }
  }
}

// Trailing edge throttle (выполнять и после последнего вызова):
function throttleWithTrailing(fn, interval) {
  let lastCallTime = 0
  let timerId = null

  return function(...args) {
    const now = Date.now()
    const remaining = interval - (now - lastCallTime)

    if (remaining <= 0) {
      clearTimeout(timerId)
      timerId = null
      lastCallTime = now
      fn.apply(this, args)
    } else if (!timerId) {
      // Запланировать вызов в конце интервала
      timerId = setTimeout(() => {
        lastCallTime = Date.now()
        timerId = null
        fn.apply(this, args)
      }, remaining)
    }
  }
}

Cancellable debounce

function debounce(fn, delay) {
  let timerId = null

  function debounced(...args) {
    clearTimeout(timerId)
    timerId = setTimeout(() => {
      fn.apply(this, args)
      timerId = null
    }, delay)
  }

  // Метод отмены
  debounced.cancel = function() {
    clearTimeout(timerId)
    timerId = null
  }

  // Немедленный вызов (flush)
  debounced.flush = function() {
    if (timerId !== null) {
      clearTimeout(timerId)
      timerId = null
      fn()
    }
  }

  return debounced
}

Leading edge debounce

По умолчанию debounce — trailing (вызов после паузы). Leading — вызов сразу, потом пауза:

function debounce(fn, delay, { leading = false, trailing = true } = {}) {
  let timerId = null
  let leadingCalled = false

  return function(...args) {
    if (leading && !timerId && !leadingCalled) {
      fn.apply(this, args)
      leadingCalled = true
    }

    clearTimeout(timerId)

    timerId = setTimeout(() => {
      if (trailing && leadingCalled) {
        // не вызываем trailing если leading уже вызвал
      } else if (trailing) {
        fn.apply(this, args)
      }
      timerId = null
      leadingCalled = false
    }, delay)
  }
}

Ключевые различия

| Критерий | Debounce | Throttle |

|----------|----------|----------|

| Принцип | Ждёт паузу N ms | Не чаще раза в N ms |

| Когда вызывать | После окончания ввода | При непрерывных событиях |

| Примеры | Поиск, resize | Scroll, mousemove, кнопка |

| Количество вызовов | 1 (в конце) | Равномерно за период |

Связанные уроки курса

  • setTimeout и setInterval — таймеры, которые лежат в основе обоих паттернов
  • Замыкания — debounce и throttle возвращают замыкания, хранящие timerId и lastCallTime
  • Как отвечать на собеседовании

    Начни с объяснения разницы на бытовом примере: «debounce — как лифт, который ждёт, пока все зайдут; throttle — как светофор, переключающийся строго по времени». Затем напиши реализацию с нуля. Упомяни leading/trailing edge и cancellable версию — это покажет глубину понимания. Обязательно скажи про конкретные use cases: debounce для поиска, throttle для scroll.

    Красные флаги ответа

  • Путаница между debounce и throttle или незнание разницы — базовое требование для любого JS-разработчика
  • Реализация только через setTimeout без clearTimeout — функция будет вызываться многократно, не будет работать debounce
  • Незнание про this и args — функция теряет контекст вызова, что критично для методов объектов
  • Примеры

    Реализация debounce и throttle с тестированием на симулированных вызовах

    // ===== DEBOUNCE =====
    function debounce(fn, delay) {
      let timerId = null
    
      function debounced(...args) {
        clearTimeout(timerId)
        timerId = setTimeout(() => {
          fn.apply(this, args)
          timerId = null
        }, delay)
      }
    
      debounced.cancel = function() {
        clearTimeout(timerId)
        timerId = null
      }
    
      return debounced
    }
    
    // ===== THROTTLE =====
    function throttle(fn, interval) {
      let lastCallTime = 0
      let timerId = null
    
      return function(...args) {
        const now = Date.now()
        const remaining = interval - (now - lastCallTime)
    
        if (remaining <= 0) {
          if (timerId) {
            clearTimeout(timerId)
            timerId = null
          }
          lastCallTime = now
          fn.apply(this, args)
        } else if (!timerId) {
          timerId = setTimeout(() => {
            lastCallTime = Date.now()
            timerId = null
            fn.apply(this, args)
          }, remaining)
        }
      }
    }
    
    // ===== ТЕСТИРОВАНИЕ =====
    
    // Тест debounce: симулируем быструю печать
    console.log('=== Debounce тест ===')
    const callLog = []
    
    const debouncedFn = debounce((val) => {
      callLog.push({ type: 'debounce', val, time: Date.now() })
      console.log('Debounce вызван с:', val)
    }, 100)
    
    // Быстрые вызовы — должен выполниться только последний
    const startTime = Date.now()
    debouncedFn('a')   // отменяется
    debouncedFn('ab')  // отменяется
    debouncedFn('abc') // выполнится через 100ms
    
    setTimeout(() => {
      console.log('После паузы вызовов:', callLog.length, '(ожидаем 1)')
    }, 200)
    
    // Тест throttle: симулируем scroll
    console.log('\n=== Throttle тест ===')
    const throttleLog = []
    
    const throttledFn = throttle((pos) => {
      throttleLog.push(pos)
      console.log('Throttle вызван, позиция:', pos)
    }, 100)
    
    // Вызовы каждые 20ms в течение 300ms
    let callCount = 0
    const throttleInterval = setInterval(() => {
      callCount++
      throttledFn(callCount * 20)
      if (callCount >= 15) {
        clearInterval(throttleInterval)
        setTimeout(() => {
          console.log('Всего событий:', 15, '| Вызовов функции:', throttleLog.length, '(ожидаем ~3-4)')
        }, 150)
      }
    }, 20)
    
    // ===== СРАВНЕНИЕ ПОВЕДЕНИЯ =====
    console.log('\n=== Разница на практике ===')
    console.log('debounce: поиск - запрос к API только после паузы ввода')
    console.log('throttle: scroll - обновление UI не чаще 1 раза в 100ms')
    
    // Демо: замер производительности с мемоизацией
    function expensiveCalc(n) {
      // имитация тяжёлых вычислений
      let result = 0
      for (let i = 0; i < n; i++) result += i
      return result
    }
    
    const throttledCalc = throttle((n) => {
      const result = expensiveCalc(n)
      console.log('Результат вычислений:', result)
    }, 50)
    
    throttledCalc(1000)  // выполнится сразу
    throttledCalc(2000)  // пропустится (в рамках 50ms)
    throttledCalc(3000)  // пропустится
    setTimeout(() => throttledCalc(4000), 60)  // выполнится (прошло 60ms > 50ms)

    Реализуй debounce и throttle

    Краткий ответ

    Debounce откладывает выполнение функции до тех пор, пока не пройдёт N миллисекунд после последнего вызова — идеален для поиска и resize. Throttle гарантирует, что функция вызывается не чаще одного раза за N миллисекунд — идеален для scroll и кликов. Оба паттерна реализуются через замыкания и таймеры.

    Полный разбор

    Debounce — «подождать тишины»

    Представь строку поиска: пользователь печатает «iPhone 15 Pro». Без debounce будет 13 запросов к API. С debounce — только один, когда пользователь остановился.

    function debounce(fn, delay) {
      let timerId = null
    
      return function(...args) {
        // Отменяем предыдущий таймер при каждом вызове
        clearTimeout(timerId)
    
        // Запускаем новый — выполнится только если не будет новых вызовов
        timerId = setTimeout(() => {
          fn.apply(this, args)
          timerId = null
        }, delay)
      }
    }
    
    // Использование
    const search = debounce((query) => {
      console.log('Запрос к API:', query)
    }, 300)
    
    search('i')       // сброс, новый таймер
    search('ip')      // сброс, новый таймер
    search('iph')     // сброс, новый таймер
    // через 300ms: 'Запрос к API: iph'

    Throttle — «не чаще раза в N ms»

    Scroll-handler срабатывает 100+ раз в секунду. Throttle ограничивает до 1 раза в 100ms.

    function throttle(fn, interval) {
      let lastCallTime = 0
    
      return function(...args) {
        const now = Date.now()
    
        if (now - lastCallTime >= interval) {
          lastCallTime = now
          fn.apply(this, args)
        }
      }
    }
    
    // Trailing edge throttle (выполнять и после последнего вызова):
    function throttleWithTrailing(fn, interval) {
      let lastCallTime = 0
      let timerId = null
    
      return function(...args) {
        const now = Date.now()
        const remaining = interval - (now - lastCallTime)
    
        if (remaining <= 0) {
          clearTimeout(timerId)
          timerId = null
          lastCallTime = now
          fn.apply(this, args)
        } else if (!timerId) {
          // Запланировать вызов в конце интервала
          timerId = setTimeout(() => {
            lastCallTime = Date.now()
            timerId = null
            fn.apply(this, args)
          }, remaining)
        }
      }
    }

    Cancellable debounce

    function debounce(fn, delay) {
      let timerId = null
    
      function debounced(...args) {
        clearTimeout(timerId)
        timerId = setTimeout(() => {
          fn.apply(this, args)
          timerId = null
        }, delay)
      }
    
      // Метод отмены
      debounced.cancel = function() {
        clearTimeout(timerId)
        timerId = null
      }
    
      // Немедленный вызов (flush)
      debounced.flush = function() {
        if (timerId !== null) {
          clearTimeout(timerId)
          timerId = null
          fn()
        }
      }
    
      return debounced
    }

    Leading edge debounce

    По умолчанию debounce — trailing (вызов после паузы). Leading — вызов сразу, потом пауза:

    function debounce(fn, delay, { leading = false, trailing = true } = {}) {
      let timerId = null
      let leadingCalled = false
    
      return function(...args) {
        if (leading && !timerId && !leadingCalled) {
          fn.apply(this, args)
          leadingCalled = true
        }
    
        clearTimeout(timerId)
    
        timerId = setTimeout(() => {
          if (trailing && leadingCalled) {
            // не вызываем trailing если leading уже вызвал
          } else if (trailing) {
            fn.apply(this, args)
          }
          timerId = null
          leadingCalled = false
        }, delay)
      }
    }

    Ключевые различия

    | Критерий | Debounce | Throttle |

    |----------|----------|----------|

    | Принцип | Ждёт паузу N ms | Не чаще раза в N ms |

    | Когда вызывать | После окончания ввода | При непрерывных событиях |

    | Примеры | Поиск, resize | Scroll, mousemove, кнопка |

    | Количество вызовов | 1 (в конце) | Равномерно за период |

    Связанные уроки курса

  • setTimeout и setInterval — таймеры, которые лежат в основе обоих паттернов
  • Замыкания — debounce и throttle возвращают замыкания, хранящие timerId и lastCallTime
  • Как отвечать на собеседовании

    Начни с объяснения разницы на бытовом примере: «debounce — как лифт, который ждёт, пока все зайдут; throttle — как светофор, переключающийся строго по времени». Затем напиши реализацию с нуля. Упомяни leading/trailing edge и cancellable версию — это покажет глубину понимания. Обязательно скажи про конкретные use cases: debounce для поиска, throttle для scroll.

    Красные флаги ответа

  • Путаница между debounce и throttle или незнание разницы — базовое требование для любого JS-разработчика
  • Реализация только через setTimeout без clearTimeout — функция будет вызываться многократно, не будет работать debounce
  • Незнание про this и args — функция теряет контекст вызова, что критично для методов объектов
  • Примеры

    Реализация debounce и throttle с тестированием на симулированных вызовах

    // ===== DEBOUNCE =====
    function debounce(fn, delay) {
      let timerId = null
    
      function debounced(...args) {
        clearTimeout(timerId)
        timerId = setTimeout(() => {
          fn.apply(this, args)
          timerId = null
        }, delay)
      }
    
      debounced.cancel = function() {
        clearTimeout(timerId)
        timerId = null
      }
    
      return debounced
    }
    
    // ===== THROTTLE =====
    function throttle(fn, interval) {
      let lastCallTime = 0
      let timerId = null
    
      return function(...args) {
        const now = Date.now()
        const remaining = interval - (now - lastCallTime)
    
        if (remaining <= 0) {
          if (timerId) {
            clearTimeout(timerId)
            timerId = null
          }
          lastCallTime = now
          fn.apply(this, args)
        } else if (!timerId) {
          timerId = setTimeout(() => {
            lastCallTime = Date.now()
            timerId = null
            fn.apply(this, args)
          }, remaining)
        }
      }
    }
    
    // ===== ТЕСТИРОВАНИЕ =====
    
    // Тест debounce: симулируем быструю печать
    console.log('=== Debounce тест ===')
    const callLog = []
    
    const debouncedFn = debounce((val) => {
      callLog.push({ type: 'debounce', val, time: Date.now() })
      console.log('Debounce вызван с:', val)
    }, 100)
    
    // Быстрые вызовы — должен выполниться только последний
    const startTime = Date.now()
    debouncedFn('a')   // отменяется
    debouncedFn('ab')  // отменяется
    debouncedFn('abc') // выполнится через 100ms
    
    setTimeout(() => {
      console.log('После паузы вызовов:', callLog.length, '(ожидаем 1)')
    }, 200)
    
    // Тест throttle: симулируем scroll
    console.log('\n=== Throttle тест ===')
    const throttleLog = []
    
    const throttledFn = throttle((pos) => {
      throttleLog.push(pos)
      console.log('Throttle вызван, позиция:', pos)
    }, 100)
    
    // Вызовы каждые 20ms в течение 300ms
    let callCount = 0
    const throttleInterval = setInterval(() => {
      callCount++
      throttledFn(callCount * 20)
      if (callCount >= 15) {
        clearInterval(throttleInterval)
        setTimeout(() => {
          console.log('Всего событий:', 15, '| Вызовов функции:', throttleLog.length, '(ожидаем ~3-4)')
        }, 150)
      }
    }, 20)
    
    // ===== СРАВНЕНИЕ ПОВЕДЕНИЯ =====
    console.log('\n=== Разница на практике ===')
    console.log('debounce: поиск - запрос к API только после паузы ввода')
    console.log('throttle: scroll - обновление UI не чаще 1 раза в 100ms')
    
    // Демо: замер производительности с мемоизацией
    function expensiveCalc(n) {
      // имитация тяжёлых вычислений
      let result = 0
      for (let i = 0; i < n; i++) result += i
      return result
    }
    
    const throttledCalc = throttle((n) => {
      const result = expensiveCalc(n)
      console.log('Результат вычислений:', result)
    }, 50)
    
    throttledCalc(1000)  // выполнится сразу
    throttledCalc(2000)  // пропустится (в рамках 50ms)
    throttledCalc(3000)  // пропустится
    setTimeout(() => throttledCalc(4000), 60)  // выполнится (прошло 60ms > 50ms)

    Задание

    Реализуй функции debounce(fn, delay) и throttle(fn, interval) с нуля. Debounce должен откладывать вызов fn до тех пор, пока не пройдёт delay мс после последнего вызова. Throttle должен вызывать fn не чаще одного раза за interval мс. Добавь метод cancel() к debounce.

    Подсказка

    debounce: храни timerId в замыкании, каждый новый вызов делает clearTimeout(timerId) и setTimeout заново. throttle: храни lastCallTime = 0, вызывай fn только если Date.now() - lastCallTime >= interval.

    Загружаем среду выполнения...
    Загружаем AI-помощника...