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

Колбэки и асинхронность

Представь интернет-магазин: пользователь нажимает «Оплатить», и сайт обращается к платёжному шлюзу. Ответ может прийти через 300ms или через 3 секунды. Если бы браузер просто «ждал» — страница бы зависла, кнопки перестали реагировать, пользователь бы закрыл вкладку. Колбэки — первое решение этой проблемы.

Какую проблему решает

JavaScript однопоточный: в один момент выполняется только одна операция. Для операций, которые занимают время (сеть, файлы, таймеры), нужен механизм «запусти и вызови меня когда готово». Этот механизм — колбэк-функция.

На основе предыдущих уроков

  • Функции — функции как значения
  • Function Expression — передача функций как аргументов
  • Стрелочные функции — краткий синтаксис колбэков
  • setTimeout — первый асинхронный механизм
  • Синхронный vs асинхронный код

    // Синхронно — каждая строка ждёт предыдущую
    console.log('1. Начало')
    console.log('2. Проверка корзины')
    console.log('3. Конец')
    // Вывод: 1, 2, 3 — строго по порядку
    
    // Асинхронно — setTimeout не блокирует поток
    console.log('1. Запрос к API')
    setTimeout(() => console.log('2. Ответ от API'), 1000)
    console.log('3. Показываем спиннер')
    // Вывод: 1, 3, 2 — спиннер показывается сразу, не ожидая ответа

    Колбэк — функция, переданная как аргумент

    function processPayment(amount, onSuccess, onError) {
      // симуляция запроса к платёжному шлюзу
      setTimeout(() => {
        if (amount > 0) {
          onSuccess({ transactionId: 'TXN-001', amount })
        } else {
          onError(new Error('Некорректная сумма'))
        }
      }, 500)
    }
    
    processPayment(
      1500,
      result => console.log('Оплачено:', result.transactionId), // колбэк успеха
      err    => console.log('Ошибка:', err.message)             // колбэк ошибки
    )

    Error-first колбэки (Node.js стиль)

    Соглашение: первый аргумент колбэка — ошибка (null если всё ОК), второй — данные:

    function fetchOrder(orderId, callback) {
      setTimeout(() => {
        if (orderId > 0) callback(null, { id: orderId, status: 'delivered' })
        else callback(new Error('Заказ не найден'))
      }, 100)
    }
    
    fetchOrder(42, (err, order) => {
      if (err) return console.log('Ошибка:', err.message)
      console.log('Заказ:', order.status)  // 'Заказ: delivered'
    })

    Callback Hell — пирамида судьбы

    Когда операции зависят друг от друга, колбэки вкладываются — код уходит вправо:

    // Получить пользователя → его заказы → детали заказа → доставку
    getUser(userId, (err, user) => {
      if (err) return handleError(err)
      getOrders(user.id, (err, orders) => {
        if (err) return handleError(err)
        getOrderDetails(orders[0].id, (err, details) => {
          if (err) return handleError(err)
          getDelivery(details.deliveryId, (err, delivery) => {
            if (err) return handleError(err)
            console.log('Статус доставки:', delivery.status)
            // Ещё уровень? Становится нечитаемым...
          })
        })
      })
    })

    Проблемы callback hell: код уходит вправо, ошибку нужно обрабатывать на каждом уровне, сложно добавить новый шаг, трудно читать.

    Промисы и async/await решают эту проблему

    // То же самое с async/await — читается как синхронный код
    async function getDeliveryStatus(userId) {
      try {
        const user = await getUser(userId)
        const orders = await getOrders(user.id)
        const details = await getOrderDetails(orders[0].id)
        const delivery = await getDelivery(details.deliveryId)
        console.log('Статус:', delivery.status)
      } catch (err) {
        handleError(err)  // одна точка для всех ошибок
      }
    }

    Event Loop кратко

    JS-движок постоянно проверяет очередь задач. Когда стек вызовов пуст — берёт следующую задачу из очереди. Именно так колбэки из setTimeout, fetch и событий попадают в выполнение после завершения текущего кода.

    Типичные ошибки

    Ошибка 1: забыли return после обработки ошибки

    // Неправильно — код продолжает выполняться после ошибки!
    fetchUser(id, (err, user) => {
      if (err) console.log('Ошибка:', err.message)
      console.log(user.name)  // CRASH — user может быть undefined
    })
    
    // Правильно
    fetchUser(id, (err, user) => {
      if (err) return console.log('Ошибка:', err.message)  // return!
      console.log(user.name)
    })

    Ошибка 2: вызов колбэка несколько раз

    // Неправильно — callback вызывается дважды при ошибке
    function loadData(url, cb) {
      if (!url) {
        cb(new Error('URL не задан'))
        // забыли return — код продолжает выполняться
      }
      fetch(url).then(r => r.json()).then(data => cb(null, data))
    }
    
    // Правильно
    function loadData(url, cb) {
      if (!url) return cb(new Error('URL не задан'))
      fetch(url).then(r => r.json()).then(data => cb(null, data))
    }

    Ошибка 3: синхронный колбэк в асинхронном API

    let result
    fetchData((err, data) => {
      result = data  // данные придут позже!
    })
    console.log(result)  // undefined — данные ещё не пришли

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

  • Node.js fs.readFile — классический error-first колбэк
  • Array.forEach/map/filter — синхронные колбэки для трансформации данных
  • setTimeout/setInterval — отложенные и периодические задачи
  • addEventListener — обработка пользовательских событий
  • Колбэки — основа понимания промисов и async/await
  • Примеры

    Callback hell vs промисы — загрузка данных пользователя из интернет-магазина

    // Симуляция асинхронных запросов к API магазина
    function delay(ms, data, fail = false) {
      return new Promise((resolve, reject) =>
        setTimeout(() => fail ? reject(new Error('Сеть недоступна')) : resolve(data), ms)
      )
    }
    
    // === Callback hell: загрузить пользователя → заказы → статус доставки ===
    function loadWithCallbacks(userId) {
      // Имитация: получаем пользователя
      setTimeout((err, user) => {
        err = null; user = { id: userId, name: 'Иван' }
        if (err) return console.error('Ошибка пользователя:', err)
    
        // Имитация: получаем заказы
        setTimeout((err2, orders) => {
          err2 = null; orders = [{ id: 1, userId, total: 1500 }]
          if (err2) return console.error('Ошибка заказов:', err2)
    
          // Имитация: получаем доставку
          setTimeout(() => {
            const delivery = { orderId: 1, status: 'В пути' }
            console.log(`[${user.name}] Заказ #${orders[0].id}: ${delivery.status}`)
            // Если нужно ещё — ещё один уровень вложенности...
          }, 100)
        }, 100)
      }, 100)
    }
    
    // === Промисы: плоская читаемая цепочка ===
    async function loadWithPromises(userId) {
      try {
        const user    = await delay(100, { id: userId, name: 'Иван' })
        const orders  = await delay(100, [{ id: 1, userId, total: 1500 }])
        const delivery = await delay(100, { orderId: 1, status: 'В пути' })
    
        console.log(`[${user.name}] Заказ #${orders[0].id}: ${delivery.status}`)
        // '[Иван] Заказ #1: В пути'
      } catch (err) {
        console.error('Ошибка на любом шаге:', err.message)
      }
    }
    
    loadWithPromises(42)
    
    // === Практический пример: repeat — вызов функции N раз с паузой ===
    function repeat(fn, times, delayMs) {
      if (times <= 0) return
      setTimeout(() => {
        fn()
        repeat(fn, times - 1, delayMs)  // рекурсивный вызов
      }, delayMs)
    }
    
    let tick = 0
    repeat(() => {
      tick++
      console.log(`Tick ${tick}`)
    }, 3, 200)
    // Через 200ms: 'Tick 1'
    // Через 400ms: 'Tick 2'
    // Через 600ms: 'Tick 3'

    Колбэки и асинхронность

    Представь интернет-магазин: пользователь нажимает «Оплатить», и сайт обращается к платёжному шлюзу. Ответ может прийти через 300ms или через 3 секунды. Если бы браузер просто «ждал» — страница бы зависла, кнопки перестали реагировать, пользователь бы закрыл вкладку. Колбэки — первое решение этой проблемы.

    Какую проблему решает

    JavaScript однопоточный: в один момент выполняется только одна операция. Для операций, которые занимают время (сеть, файлы, таймеры), нужен механизм «запусти и вызови меня когда готово». Этот механизм — колбэк-функция.

    На основе предыдущих уроков

  • Функции — функции как значения
  • Function Expression — передача функций как аргументов
  • Стрелочные функции — краткий синтаксис колбэков
  • setTimeout — первый асинхронный механизм
  • Синхронный vs асинхронный код

    // Синхронно — каждая строка ждёт предыдущую
    console.log('1. Начало')
    console.log('2. Проверка корзины')
    console.log('3. Конец')
    // Вывод: 1, 2, 3 — строго по порядку
    
    // Асинхронно — setTimeout не блокирует поток
    console.log('1. Запрос к API')
    setTimeout(() => console.log('2. Ответ от API'), 1000)
    console.log('3. Показываем спиннер')
    // Вывод: 1, 3, 2 — спиннер показывается сразу, не ожидая ответа

    Колбэк — функция, переданная как аргумент

    function processPayment(amount, onSuccess, onError) {
      // симуляция запроса к платёжному шлюзу
      setTimeout(() => {
        if (amount > 0) {
          onSuccess({ transactionId: 'TXN-001', amount })
        } else {
          onError(new Error('Некорректная сумма'))
        }
      }, 500)
    }
    
    processPayment(
      1500,
      result => console.log('Оплачено:', result.transactionId), // колбэк успеха
      err    => console.log('Ошибка:', err.message)             // колбэк ошибки
    )

    Error-first колбэки (Node.js стиль)

    Соглашение: первый аргумент колбэка — ошибка (null если всё ОК), второй — данные:

    function fetchOrder(orderId, callback) {
      setTimeout(() => {
        if (orderId > 0) callback(null, { id: orderId, status: 'delivered' })
        else callback(new Error('Заказ не найден'))
      }, 100)
    }
    
    fetchOrder(42, (err, order) => {
      if (err) return console.log('Ошибка:', err.message)
      console.log('Заказ:', order.status)  // 'Заказ: delivered'
    })

    Callback Hell — пирамида судьбы

    Когда операции зависят друг от друга, колбэки вкладываются — код уходит вправо:

    // Получить пользователя → его заказы → детали заказа → доставку
    getUser(userId, (err, user) => {
      if (err) return handleError(err)
      getOrders(user.id, (err, orders) => {
        if (err) return handleError(err)
        getOrderDetails(orders[0].id, (err, details) => {
          if (err) return handleError(err)
          getDelivery(details.deliveryId, (err, delivery) => {
            if (err) return handleError(err)
            console.log('Статус доставки:', delivery.status)
            // Ещё уровень? Становится нечитаемым...
          })
        })
      })
    })

    Проблемы callback hell: код уходит вправо, ошибку нужно обрабатывать на каждом уровне, сложно добавить новый шаг, трудно читать.

    Промисы и async/await решают эту проблему

    // То же самое с async/await — читается как синхронный код
    async function getDeliveryStatus(userId) {
      try {
        const user = await getUser(userId)
        const orders = await getOrders(user.id)
        const details = await getOrderDetails(orders[0].id)
        const delivery = await getDelivery(details.deliveryId)
        console.log('Статус:', delivery.status)
      } catch (err) {
        handleError(err)  // одна точка для всех ошибок
      }
    }

    Event Loop кратко

    JS-движок постоянно проверяет очередь задач. Когда стек вызовов пуст — берёт следующую задачу из очереди. Именно так колбэки из setTimeout, fetch и событий попадают в выполнение после завершения текущего кода.

    Типичные ошибки

    Ошибка 1: забыли return после обработки ошибки

    // Неправильно — код продолжает выполняться после ошибки!
    fetchUser(id, (err, user) => {
      if (err) console.log('Ошибка:', err.message)
      console.log(user.name)  // CRASH — user может быть undefined
    })
    
    // Правильно
    fetchUser(id, (err, user) => {
      if (err) return console.log('Ошибка:', err.message)  // return!
      console.log(user.name)
    })

    Ошибка 2: вызов колбэка несколько раз

    // Неправильно — callback вызывается дважды при ошибке
    function loadData(url, cb) {
      if (!url) {
        cb(new Error('URL не задан'))
        // забыли return — код продолжает выполняться
      }
      fetch(url).then(r => r.json()).then(data => cb(null, data))
    }
    
    // Правильно
    function loadData(url, cb) {
      if (!url) return cb(new Error('URL не задан'))
      fetch(url).then(r => r.json()).then(data => cb(null, data))
    }

    Ошибка 3: синхронный колбэк в асинхронном API

    let result
    fetchData((err, data) => {
      result = data  // данные придут позже!
    })
    console.log(result)  // undefined — данные ещё не пришли

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

  • Node.js fs.readFile — классический error-first колбэк
  • Array.forEach/map/filter — синхронные колбэки для трансформации данных
  • setTimeout/setInterval — отложенные и периодические задачи
  • addEventListener — обработка пользовательских событий
  • Колбэки — основа понимания промисов и async/await
  • Примеры

    Callback hell vs промисы — загрузка данных пользователя из интернет-магазина

    // Симуляция асинхронных запросов к API магазина
    function delay(ms, data, fail = false) {
      return new Promise((resolve, reject) =>
        setTimeout(() => fail ? reject(new Error('Сеть недоступна')) : resolve(data), ms)
      )
    }
    
    // === Callback hell: загрузить пользователя → заказы → статус доставки ===
    function loadWithCallbacks(userId) {
      // Имитация: получаем пользователя
      setTimeout((err, user) => {
        err = null; user = { id: userId, name: 'Иван' }
        if (err) return console.error('Ошибка пользователя:', err)
    
        // Имитация: получаем заказы
        setTimeout((err2, orders) => {
          err2 = null; orders = [{ id: 1, userId, total: 1500 }]
          if (err2) return console.error('Ошибка заказов:', err2)
    
          // Имитация: получаем доставку
          setTimeout(() => {
            const delivery = { orderId: 1, status: 'В пути' }
            console.log(`[${user.name}] Заказ #${orders[0].id}: ${delivery.status}`)
            // Если нужно ещё — ещё один уровень вложенности...
          }, 100)
        }, 100)
      }, 100)
    }
    
    // === Промисы: плоская читаемая цепочка ===
    async function loadWithPromises(userId) {
      try {
        const user    = await delay(100, { id: userId, name: 'Иван' })
        const orders  = await delay(100, [{ id: 1, userId, total: 1500 }])
        const delivery = await delay(100, { orderId: 1, status: 'В пути' })
    
        console.log(`[${user.name}] Заказ #${orders[0].id}: ${delivery.status}`)
        // '[Иван] Заказ #1: В пути'
      } catch (err) {
        console.error('Ошибка на любом шаге:', err.message)
      }
    }
    
    loadWithPromises(42)
    
    // === Практический пример: repeat — вызов функции N раз с паузой ===
    function repeat(fn, times, delayMs) {
      if (times <= 0) return
      setTimeout(() => {
        fn()
        repeat(fn, times - 1, delayMs)  // рекурсивный вызов
      }, delayMs)
    }
    
    let tick = 0
    repeat(() => {
      tick++
      console.log(`Tick ${tick}`)
    }, 3, 200)
    // Через 200ms: 'Tick 1'
    // Через 400ms: 'Tick 2'
    // Через 600ms: 'Tick 3'

    Задание

    Реализуй функцию retry(fn, times, delay) для системы мониторинга: она вызывает асинхронную функцию fn (error-first колбэк), и если та завершилась с ошибкой — повторяет попытку через delay миллисекунд, но не более times раз. При успехе или исчерпании попыток вызывает финальный колбэк.

    Подсказка

    В случае ошибки передай в callback: callback(err). При повторной попытке уменьши times: retry(fn, times - 1, delay, callback). Базовый случай: times <= 1 означает что это последняя попытка.

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