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

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

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

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

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

Цепочка промисов

Даже при использовании промисов не всегда понятно, как структурировать асинхронный код. В этом уроке мы разберём некоторые полезные практики, делающие его проще для написания и анализа. Возьмём уже знакомую нам задачку по объединению двух файлов.

import fs from 'fs';

const unionFiles = (inputPath1, inputPath2, outputPath, cb) => {
  fs.readFile(inputPath1, 'utf-8', (error1, data1) => {
    if (error1) {
      cb(error1);
      return;
    }
    fs.readFile(inputPath2, 'utf-8', (error2, data2) => {
      if (error2) {
        cb(error2);
        return;
      }
      fs.writeFile(outputPath, `${data1}${data2}`, cb);
    });
  });
}

Запомните этот код — таким вы больше никогда его не увидите ;) Сейчас мы проведём серию рефакторингов и получим в результате код, который является каноническим при работе с промисами. Итак, первая версия:

import { promises as fsp } from 'fs';

const unionFiles = (inputPath1, inputPath2, outputPath) => {
  // Промисы всегда должны возвращаться и строиться в цепочку!
  const result = fsp.readFile(inputPath1, 'utf-8')
    .then((data1) => {
      const promise = fsp.readFile(inputPath2, 'utf-8')
        .then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
      return promise;
    });
  return result; // это промис
};

Хорошая новость — код стал понятнее и уменьшился в объёме. К тому же, из него целиком ушла обработка ошибок, так как промисы обрабатывают их автоматически и, если вызывающий код захочет их перехватывать, то сделает это самостоятельно через метод catch(). Но есть и плохая новость — код всё ещё структурирован, как колбеки, "лесенкой". В этом коде не учитывается свойство промисов, связанное с возвратом из then().

import { promises as fsp } from 'fs';

const unionFiles = (inputPath1, inputPath2, outputPath) => {
  const result = fsp.readFile(inputPath1, 'utf-8')
    .then((data1) => fsp.readFile(inputPath2, 'utf-8'))
    // then ниже берется от промиса readFile
    .then((data2) => fsp.writeFile(outputPath, `${<как сюда попадет data1?>}${data2}`));
  return result;
};

Эта версия совсем плоская, именно к такому коду нужно стремиться в промисах. Но она таит в себе одну проблему. Если где-то в цепочке ниже нужны данные, которые были получены сверху, то придется протаскивать их сквозь всю цепочку. В примере выше это результат чтения первого файла. Переменная data1 недоступна в том месте, где происходит запись в файл. Основной выход из данной ситуации — создание переменных, через которые данные будут прокинуты дальше:

import { promises as fsp } from 'fs';

const unionFiles = (inputPath1, inputPath2, outputPath) => {
  let data1;
  return fsp.readFile(inputPath1, 'utf-8')
    .then((content) => {
      data1 = content;
    })
    .then(() => fsp.readFile(inputPath2, 'utf-8'))
    .then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
};

Уже не так красиво, но всё ещё плоско. Преимущество такого подхода становится всё более и более очевидным с увеличением количества промисов. Тем более далеко не всегда нужно передавать данные дальше.

Контроль асинхронных операций

Частая ошибка при работе с промисами – потеря контроля. Посмотрите на немного измененный код из примеров выше:

import { promises as fsp } from 'fs';

// Чего здесь не хватает?
const unionFiles = (inputPath1, inputPath2, outputPath) => {
  const result = fsp.readFile(inputPath1, 'utf-8')
    .then((data1) => {
      const promise = fsp.readFile(inputPath2, 'utf-8')
        .then((data2) => fsp.writeFile(outputPath, `${data1}${data2}`));
    });
  return result;
};

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

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

Чтобы этого не происходило, нужно всегда убеждаться в непрерывности асинхронных операций. Завершение любой операции должно приводить к какой-то реакции, кто-то всегда должен ждать этого момента.

Динамическая цепочка

Иногда количество асинхронных операций заранее неизвестно, но они должны выполняться строго по очереди. Эту задачу можно решить используя циклы или свертку. Какой бы способ не был выбран, сам принцип построения цепочки не поменяется. Цепочка промисов это всегда then().then().then()....

Единственный нюанс, который нужно учесть – начальный промис, с которого начнет строится цепочка. Если такого промиса нет, то его можно создать используя функцию Promise.resolve(). Она возвращает промис, который ничего не делает, но с него можно начинать свертку. Ниже пример, в котором последовательно читается набор файлов и возвращается массив их содержимого:

const filePaths = /* список путей до файлов */;

// Эта функция принимает на вход необязательное значение,
// которое появится в promise.then((<тут>) => ...)
// Начальное значение в данном случае – массив,
// в котором накапливаются данные из файлов
const initPromise = Promise.resolve([]);

// В then отдается функция, а не ее вызов!
const promise = filePaths.reduce((acc, path) => {
  // Аккумулятор – всегда промис, внутри которого массив с содержимым файлов
  const newAcc = acc.then((contents) =>
    // Читаем файл и добавляем его данные в аккумулятор
    fsp.readFile(path).then((data) => contents.concat(data)));
  return newAcc;
}, initPromise);

// Если надо, продолжаем обработку
promise.then((contents) => /* обрабатываем все данные полученные из файлов */)

<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

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

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

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