В Google Docs автосохранение происходит каждые несколько секунд — это setInterval. Всплывающее уведомление «Файл сохранён» исчезает через 3 секунды — это setTimeout. Поле поиска делает запрос только через 300мс после последнего нажатия клавиши (дебаунс) — это тоже setTimeout. Таймеры — основа любого асинхронного UX.
// Выполнить через 2000мс (2 секунды)
const timerId = setTimeout(() => {
console.log('Уведомление исчезло')
}, 2000)
// Отменить до срабатывания:
clearTimeout(timerId)
// setTimeout(fn, 0) — отложить на следующую итерацию event loop:
console.log('1')
setTimeout(() => console.log('3'), 0)
console.log('2')
// 1, 2, 3 — коллбэк всегда после синхронного кодаlet ticks = 0
const intervalId = setInterval(() => {
ticks++
console.log(`Тик ${ticks}`)
if (ticks >= 3) {
clearInterval(intervalId) // остановить
console.log('Остановлено')
}
}, 1000)
// Тик 1 (через 1с)
// Тик 2 (через 2с)
// Тик 3 (через 3с)
// ОстановленоsetInterval запускает следующий вызов независимо от того, завершился ли предыдущий. Вложенный setTimeout точнее:
// setInterval — следующий запуск через 1с от предыдущего старта
// Если doWork() занимает 800мс, реальный интервал = 200мс!
setInterval(doWork, 1000)
// Вложенный setTimeout — следующий запуск через 1с от ЗАВЕРШЕНИЯ
function schedule() {
doWork()
setTimeout(schedule, 1000) // запустить снова через 1с после окончания
}
schedule()Выполняет функцию только после паузы в событиях. Идеален для поиска, автосохранения:
function debounce(fn, delay) {
let timerId
return function(...args) {
clearTimeout(timerId) // отменяем предыдущий таймер
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
const saveDocument = debounce(() => {
console.log('Документ сохранён')
}, 1000)
// Пользователь быстро печатает:
saveDocument() // таймер сброшен
saveDocument() // таймер сброшен
saveDocument() // через 1с после этого — 'Документ сохранён'Выполняет функцию не чаще раза в N миллисекунд. Идеален для scroll, resize:
function throttle(fn, limit) {
let lastCall = 0
return function(...args) {
const now = Date.now()
if (now - lastCall >= limit) {
lastCall = now
return fn.apply(this, args)
}
}
}Ошибка 1: this теряется в коллбэке
// Сломано:
const timer = {
count: 0,
start() {
setInterval(function() {
this.count++ // this не timer — это undefined или window
}, 100)
}
}
// Исправлено — стрелочная функция:
start() {
setInterval(() => {
this.count++ // this = timer (из start)
}, 100)
}Ошибка 2: утечка памяти — незакрытый интервал
// Сломано — при каждом клике создаётся новый интервал:
button.addEventListener('click', () => {
setInterval(updateCounter, 1000) // интервалы накапливаются!
})
// Исправлено — проверяй и очищай:
let intervalId = null
button.addEventListener('click', () => {
if (intervalId) clearInterval(intervalId)
intervalId = setInterval(updateCounter, 1000)
})Ошибка 3: ожидание точного времени
// НЕЛЬЗЯ полагаться на точность таймеров:
// setTimeout(() => ..., 1000) сработает через >=1000мс, но не ровно 1000
// Для точного времени используй Date.now() и корректируй дрейфsetTimeout(() => toast.remove(), 3000)Debounce для поиска и Throttle для скролла
// Debounce — задержка выполнения до конца ввода
function debounce(fn, delay) {
let timerId
return function(...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
// Throttle — выполнять не чаще раза в limit мс
function throttle(fn, limit) {
let lastCall = 0
return function(...args) {
const now = Date.now()
if (now - lastCall >= limit) {
lastCall = now
return fn.apply(this, args)
}
}
}
// Симуляция быстрого ввода в поиск
const searchApi = debounce((query) => {
console.log(`[API] Поиск: "${query}"`)
}, 300)
// Симулируем набор текста с интервалом 100мс
const letters = ['J', 'JS', 'JSc', 'JScp', 'JScri', 'JScrip', 'JScript']
letters.forEach((text, i) => {
setTimeout(() => searchApi(text), i * 100)
})
// Через 100мс: debounce перезапускается
// Через 200мс: debounce перезапускается
// ...
// Через 930мс: [API] Поиск: "JScript" — только один запрос!
// Throttle для обновления прогресс-бара при скролле
let scrollY = 0
const updateProgress = throttle(() => {
const percent = Math.min(100, Math.round(scrollY / 10))
console.log(`Прогресс: ${percent}%`)
}, 200)
// Симулируем события скролла
;[100, 200, 250, 300, 310, 400, 500].forEach(y => {
scrollY = y
updateProgress()
})
// Прогресс: 10% (y=100)
// Прогресс: 25% (y=250, следующий после 200мс)
// Прогресс: 50% (y=500)
// Промис-обёртка вокруг setTimeout
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function showNotification(message, durationMs = 3000) {
console.log(`Уведомление: ${message}`)
await delay(durationMs)
console.log('Уведомление скрыто')
}
showNotification('Файл сохранён!', 100)
// Уведомление: Файл сохранён!
// (через 100мс) Уведомление скрытоВ Google Docs автосохранение происходит каждые несколько секунд — это setInterval. Всплывающее уведомление «Файл сохранён» исчезает через 3 секунды — это setTimeout. Поле поиска делает запрос только через 300мс после последнего нажатия клавиши (дебаунс) — это тоже setTimeout. Таймеры — основа любого асинхронного UX.
// Выполнить через 2000мс (2 секунды)
const timerId = setTimeout(() => {
console.log('Уведомление исчезло')
}, 2000)
// Отменить до срабатывания:
clearTimeout(timerId)
// setTimeout(fn, 0) — отложить на следующую итерацию event loop:
console.log('1')
setTimeout(() => console.log('3'), 0)
console.log('2')
// 1, 2, 3 — коллбэк всегда после синхронного кодаlet ticks = 0
const intervalId = setInterval(() => {
ticks++
console.log(`Тик ${ticks}`)
if (ticks >= 3) {
clearInterval(intervalId) // остановить
console.log('Остановлено')
}
}, 1000)
// Тик 1 (через 1с)
// Тик 2 (через 2с)
// Тик 3 (через 3с)
// ОстановленоsetInterval запускает следующий вызов независимо от того, завершился ли предыдущий. Вложенный setTimeout точнее:
// setInterval — следующий запуск через 1с от предыдущего старта
// Если doWork() занимает 800мс, реальный интервал = 200мс!
setInterval(doWork, 1000)
// Вложенный setTimeout — следующий запуск через 1с от ЗАВЕРШЕНИЯ
function schedule() {
doWork()
setTimeout(schedule, 1000) // запустить снова через 1с после окончания
}
schedule()Выполняет функцию только после паузы в событиях. Идеален для поиска, автосохранения:
function debounce(fn, delay) {
let timerId
return function(...args) {
clearTimeout(timerId) // отменяем предыдущий таймер
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
const saveDocument = debounce(() => {
console.log('Документ сохранён')
}, 1000)
// Пользователь быстро печатает:
saveDocument() // таймер сброшен
saveDocument() // таймер сброшен
saveDocument() // через 1с после этого — 'Документ сохранён'Выполняет функцию не чаще раза в N миллисекунд. Идеален для scroll, resize:
function throttle(fn, limit) {
let lastCall = 0
return function(...args) {
const now = Date.now()
if (now - lastCall >= limit) {
lastCall = now
return fn.apply(this, args)
}
}
}Ошибка 1: this теряется в коллбэке
// Сломано:
const timer = {
count: 0,
start() {
setInterval(function() {
this.count++ // this не timer — это undefined или window
}, 100)
}
}
// Исправлено — стрелочная функция:
start() {
setInterval(() => {
this.count++ // this = timer (из start)
}, 100)
}Ошибка 2: утечка памяти — незакрытый интервал
// Сломано — при каждом клике создаётся новый интервал:
button.addEventListener('click', () => {
setInterval(updateCounter, 1000) // интервалы накапливаются!
})
// Исправлено — проверяй и очищай:
let intervalId = null
button.addEventListener('click', () => {
if (intervalId) clearInterval(intervalId)
intervalId = setInterval(updateCounter, 1000)
})Ошибка 3: ожидание точного времени
// НЕЛЬЗЯ полагаться на точность таймеров:
// setTimeout(() => ..., 1000) сработает через >=1000мс, но не ровно 1000
// Для точного времени используй Date.now() и корректируй дрейфsetTimeout(() => toast.remove(), 3000)Debounce для поиска и Throttle для скролла
// Debounce — задержка выполнения до конца ввода
function debounce(fn, delay) {
let timerId
return function(...args) {
clearTimeout(timerId)
timerId = setTimeout(() => fn.apply(this, args), delay)
}
}
// Throttle — выполнять не чаще раза в limit мс
function throttle(fn, limit) {
let lastCall = 0
return function(...args) {
const now = Date.now()
if (now - lastCall >= limit) {
lastCall = now
return fn.apply(this, args)
}
}
}
// Симуляция быстрого ввода в поиск
const searchApi = debounce((query) => {
console.log(`[API] Поиск: "${query}"`)
}, 300)
// Симулируем набор текста с интервалом 100мс
const letters = ['J', 'JS', 'JSc', 'JScp', 'JScri', 'JScrip', 'JScript']
letters.forEach((text, i) => {
setTimeout(() => searchApi(text), i * 100)
})
// Через 100мс: debounce перезапускается
// Через 200мс: debounce перезапускается
// ...
// Через 930мс: [API] Поиск: "JScript" — только один запрос!
// Throttle для обновления прогресс-бара при скролле
let scrollY = 0
const updateProgress = throttle(() => {
const percent = Math.min(100, Math.round(scrollY / 10))
console.log(`Прогресс: ${percent}%`)
}, 200)
// Симулируем события скролла
;[100, 200, 250, 300, 310, 400, 500].forEach(y => {
scrollY = y
updateProgress()
})
// Прогресс: 10% (y=100)
// Прогресс: 25% (y=250, следующий после 200мс)
// Прогресс: 50% (y=500)
// Промис-обёртка вокруг setTimeout
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function showNotification(message, durationMs = 3000) {
console.log(`Уведомление: ${message}`)
await delay(durationMs)
console.log('Уведомление скрыто')
}
showNotification('Файл сохранён!', 100)
// Уведомление: Файл сохранён!
// (через 100мс) Уведомление скрытоРеализуй функцию createTimer(onTick, onComplete), которая возвращает объект таймера с методами: start(seconds) — запускает обратный отсчёт от seconds до 0, вызывая onTick(remaining) каждую секунду, при достижении 0 вызывает onComplete(), stop() — останавливает таймер, isRunning() — возвращает true если таймер работает. Нельзя запустить уже запущенный таймер.
isRunning проверяет intervalId !== null. В setInterval: onTick(remaining--) — сначала вызов, потом уменьшение (или уменьшай сразу: remaining--, onTick(remaining)). Проверь если (remaining <= 0): clearInterval и вызови onComplete.