Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос нашим менторам. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Промисы (Promise)

Асинхронный код, при всех его преимуществах, очень сложен в анализе. Буквально несколько вложенных колбеков с параллельным выполнением операций и уже практически невозможно разобраться в происходящем. Страшно представить себе большую программу в которой асинхронно все. Тысяча, другая строк кода и ни один человек не сможет понять его.

С самого начала разработчики понимали ограниченность подхода с колбеками, но понадобилось не мало времени, до того как в JavaScript появилась альтернатива – Promises. Промисы меняют способ организации кода не добавляя нового синтаксиса. При правильном использовании, они позволяют "выпрямить" асинхронный код и сделать его предельно плоским и последовательным.

Большая часть современного JavaScript кода пишется на промисах, а колбеки уходят в прошлое. Например разработчики Node.js внедрили промисы практически во все встроенные модули. Функции для работы с файловой системой построенные на промисах доступны через свойство promises модуля fs. Сравните примеры:

import fs from 'fs';

// Код на колбеках похож на лесенку

fs.readFile('./first', 'utf-8', (_error1, data1) => {
  console.log(data2);
  fs.readFile('./second', 'utf-8', (_error2, data2) => {
    console.log(data2);
    fs.readFile('./third', (_error3, data3) => {
      console.log(data3);
    });
  });
});

// Код на промисах практически плоский

// Переименование свойства promises в fsp для краткости
const { promises as fsp } = fs;

fsp.readFile('./first', 'utf-8')
  .then((data1) => console.log(data1))
  .then(() => fsp.readFile('./second', 'utf-8'))
  .then((data2) => console.log(data2))
  .then(() => fsp.readFile('./third', 'utf-8'))
  .then((data3) => console.log(data3));

Технически, промис это специальный объект, который отслеживает асинхронную операцию и хранит внутри себя ее результат. Он возвращается всеми асинхронными функциями построенными на промисах.

const promise = fsp.readFile(src, 'utf-8');

Очень важно понимать, что промис — это не результат асинхронной операции. Это объект, который отслеживает выполнение операции. Операция по-прежнему асинхронна и выполнится когда-нибудь потом.

const promise = fsp.readFile(src, 'utf-8');
// Файл еще не прочитан
console.log(promise);
// Promise { <pending> }
// pending – это состояние промиса, говорит о том что операция еще в процессе

Как получить результат выполнения асинхронной операции? Снаружи — никак, это просто невозможно. Но промис можно "продолжить", используя метод then(), в который нужно передать колбек-функцию. Параметром этого колбека и будет тот самый результат асинхронной операции

// Результат чтения файла передан в колбек-функцию переданную в then
// колбек вызовется только тогда, когда выполнится чтение файла
fsp.readFile(src, 'utf-8').then((content) => console.log(content));

Колбек именно передается внутрь then(), а не вызывается. Вызов делает уже сам промис тогда, когда выполнится асинхронная операция.

Независимо от содержимого колбек-функции, вызов then() всегда возвращает новый промис. А возврат колбек-функции становится доступным как параметр колбека следующего then(). Именно такая организация промисов позволяет строить цепочки без необходимости вкладывать вызовы друг в друга, тем самым избегая Callback Hell.

// Предположим что внутри файла был тест Hexlet
const promise = fsp.readFile(src, 'utf-8') // результат цепочки ВСЕГДА промис
  .then((content) => `go to the next then with ${content}`) // игнорируем результат операции
  .then((text) => console.log(text)); // в этот колбек, роль которого играет лог, передается значение с предыдущего then
// => go to the next then with Hexlet

Вопрос на самопроверку. Что выведется на экран, если добавить к промису выше then(console.log)?

Использование промисов внутри любой функции, автоматически делает эту функцию асинхронной. Она больше не может вызываться как обычная синхронная функция, так как в таком случае невозможно воспользоваться результатом ее работы, дождаться выполнения операции или узнать об ошибках:

// Неправильное определение

export const copy = (src, dest) => {
  fsp.readFile(src, 'utf-8')
    .then((content) => fsp.writeFile(dest, content));
};

// Использование

// делаем что-то синхронное
copy(src, dest);
// делаем что-то еще

Как-только в коде появляется асинхронность, код должен менять свою структуру. В случае колбеков он становится вложенным, в случае промисов весь код превращается в непрерывную цепочку промисов.

// Правильное определение

export const copy = (src, dest) => {
  return fsp.readFile(src, 'utf-8')
    .then((content) => fsp.writeFile(dest, content));
};

// Использование

// делаем что-то синхронное
copy(src, dest).then(() => {
  // делаем что-то еще
}).then(/* продолжаем */)
  .then(/* продолжаем */);

Неправильное использование промисов

Главное преимущестов промисов перед колбеками в том, что с их помощью асинхронный код становится немного похож на синхронный. Видно цепочку вызовов и она не растет вглубь. По крайней мере в теории. На практике же, промисы используются не всегда правильно. Посмотрите на код:


fsp.readFile('./first', 'utf-8')
  .then((data1) => {
    console.log(data1);
    // Читаем файл и продолжаем промис от этой, внутренней функции
    return fsp.readFile('./second', 'utf-8').then((data2) => {
      console.log(data2);
      // Читаем файл и продолжаем промис от этой, внутренней функции
      return fsp.readFile('./third', 'utf-8').then((data2) => {
        console.log(data3);
      });
    });
  });

Несмотря на то что здесь используются промисы, код выглядит даже сложнее чем с колбеками. Проблема в том как построены вызовы. Продолжение цепочки идет не от верхнего промиса, а от каждой последующей асинхронной операции. В теории промисы действительно бывают вложенными, но только там, где по другому никак. В ситуации код должен быть плоским и простым:

fsp.readFile('./first', 'utf-8')
  .then((data1) => console.log(data1))
  .then(() => fsp.readFile('./second', 'utf-8'))
  .then((data2) => console.log(data2))
  .then(() => fsp.readFile('./third', 'utf-8'))
  .then((data3) => console.log(data3));

Дополнительные материалы

  1. promisejs.org

<span class="translation_missing" title="translation missing: ru.web.courses.lessons.mentors.mentor_avatars">Mentor Avatars</span>

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят менторы из команды Хекслета или другие студенты.

Для полного доступа к курсу, нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, даст возможность обращаться за помощью к менторам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Зарегистрироваться

или войти в аккаунт

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг».