← JavaScript/setTimeout и setInterval#77 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

setTimeout и setInterval

Реальная проблема: отложенные действия в интерфейсе

В Google Docs автосохранение происходит каждые несколько секунд — это setInterval. Всплывающее уведомление «Файл сохранён» исчезает через 3 секунды — это setTimeout. Поле поиска делает запрос только через 300мс после последнего нажатия клавиши (дебаунс) — это тоже setTimeout. Таймеры — основа любого асинхронного UX.

Что решают таймеры

  • Отложить выполнение кода (показать уведомление, скрыть элемент)
  • Повторять действие с интервалом (обновление данных, анимация)
  • Дебаунс — выполнить один раз после паузы в событиях
  • Тротл — выполнять не чаще раза в N миллисекунд
  • На основе предыдущих уроков

  • «Функции» — функции как аргументы (коллбэки)
  • «Замыкания» — таймер захватывает переменные через замыкание
  • «Промисы» (впереди) — setTimeout лежит в основе Promise.resolve().then()
  • setTimeout — один раз с задержкой

    // Выполнить через 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 — коллбэк всегда после синхронного кода

    setInterval — повторять с интервалом

    let ticks = 0
    const intervalId = setInterval(() => {
      ticks++
      console.log(`Тик ${ticks}`)
    
      if (ticks >= 3) {
        clearInterval(intervalId)  // остановить
        console.log('Остановлено')
      }
    }, 1000)
    // Тик 1 (через 1с)
    // Тик 2 (через 2с)
    // Тик 3 (через 3с)
    // Остановлено

    Вложенный setTimeout vs setInterval

    setInterval запускает следующий вызов независимо от того, завершился ли предыдущий. Вложенный setTimeout точнее:

    // setInterval — следующий запуск через 1с от предыдущего старта
    // Если doWork() занимает 800мс, реальный интервал = 200мс!
    setInterval(doWork, 1000)
    
    // Вложенный setTimeout — следующий запуск через 1с от ЗАВЕРШЕНИЯ
    function schedule() {
      doWork()
      setTimeout(schedule, 1000)  // запустить снова через 1с после окончания
    }
    schedule()

    Паттерн Debounce

    Выполняет функцию только после паузы в событиях. Идеален для поиска, автосохранения:

    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с после этого — 'Документ сохранён'

    Паттерн Throttle

    Выполняет функцию не чаще раза в 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() и корректируй дрейф

    В реальных проектах

  • Автосохранение: debounce на 1-2 секунды при изменении документа
  • Поиск: debounce на 300-500мс на поле ввода
  • Polling: опрос API каждые N секунд через вложенный setTimeout
  • Анимации: requestAnimationFrame (не setTimeout) — для плавных анимаций
  • Уведомления: 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мс) Уведомление скрыто

    setTimeout и setInterval

    Реальная проблема: отложенные действия в интерфейсе

    В Google Docs автосохранение происходит каждые несколько секунд — это setInterval. Всплывающее уведомление «Файл сохранён» исчезает через 3 секунды — это setTimeout. Поле поиска делает запрос только через 300мс после последнего нажатия клавиши (дебаунс) — это тоже setTimeout. Таймеры — основа любого асинхронного UX.

    Что решают таймеры

  • Отложить выполнение кода (показать уведомление, скрыть элемент)
  • Повторять действие с интервалом (обновление данных, анимация)
  • Дебаунс — выполнить один раз после паузы в событиях
  • Тротл — выполнять не чаще раза в N миллисекунд
  • На основе предыдущих уроков

  • «Функции» — функции как аргументы (коллбэки)
  • «Замыкания» — таймер захватывает переменные через замыкание
  • «Промисы» (впереди) — setTimeout лежит в основе Promise.resolve().then()
  • setTimeout — один раз с задержкой

    // Выполнить через 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 — коллбэк всегда после синхронного кода

    setInterval — повторять с интервалом

    let ticks = 0
    const intervalId = setInterval(() => {
      ticks++
      console.log(`Тик ${ticks}`)
    
      if (ticks >= 3) {
        clearInterval(intervalId)  // остановить
        console.log('Остановлено')
      }
    }, 1000)
    // Тик 1 (через 1с)
    // Тик 2 (через 2с)
    // Тик 3 (через 3с)
    // Остановлено

    Вложенный setTimeout vs setInterval

    setInterval запускает следующий вызов независимо от того, завершился ли предыдущий. Вложенный setTimeout точнее:

    // setInterval — следующий запуск через 1с от предыдущего старта
    // Если doWork() занимает 800мс, реальный интервал = 200мс!
    setInterval(doWork, 1000)
    
    // Вложенный setTimeout — следующий запуск через 1с от ЗАВЕРШЕНИЯ
    function schedule() {
      doWork()
      setTimeout(schedule, 1000)  // запустить снова через 1с после окончания
    }
    schedule()

    Паттерн Debounce

    Выполняет функцию только после паузы в событиях. Идеален для поиска, автосохранения:

    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с после этого — 'Документ сохранён'

    Паттерн Throttle

    Выполняет функцию не чаще раза в 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() и корректируй дрейф

    В реальных проектах

  • Автосохранение: debounce на 1-2 секунды при изменении документа
  • Поиск: debounce на 300-500мс на поле ввода
  • Polling: опрос API каждые N секунд через вложенный setTimeout
  • Анимации: requestAnimationFrame (не setTimeout) — для плавных анимаций
  • Уведомления: 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.

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