**Debounce** откладывает выполнение функции до тех пор, пока не пройдёт N миллисекунд после последнего вызова — идеален для поиска и resize. **Throttle** гарантирует, что функция вызывается не чаще одного раза за N миллисекунд — идеален для scroll и кликов. Оба паттерна реализуются через замыкания и таймеры.
Представь строку поиска: пользователь печатает «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'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)
}
}
}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
}По умолчанию 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 (в конце) | Равномерно за период |
Начни с объяснения разницы на бытовом примере: «debounce — как лифт, который ждёт, пока все зайдут; throttle — как светофор, переключающийся строго по времени». Затем напиши реализацию с нуля. Упомяни leading/trailing edge и cancellable версию — это покажет глубину понимания. Обязательно скажи про конкретные use cases: debounce для поиска, throttle для scroll.
Реализация 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** откладывает выполнение функции до тех пор, пока не пройдёт N миллисекунд после последнего вызова — идеален для поиска и resize. **Throttle** гарантирует, что функция вызывается не чаще одного раза за N миллисекунд — идеален для scroll и кликов. Оба паттерна реализуются через замыкания и таймеры.
Представь строку поиска: пользователь печатает «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'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)
}
}
}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
}По умолчанию 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 (в конце) | Равномерно за период |
Начни с объяснения разницы на бытовом примере: «debounce — как лифт, который ждёт, пока все зайдут; throttle — как светофор, переключающийся строго по времени». Затем напиши реализацию с нуля. Упомяни leading/trailing edge и cancellable версию — это покажет глубину понимания. Обязательно скажи про конкретные use cases: debounce для поиска, throttle для scroll.
Реализация 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-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке