Даже при использовании промисов не всегда понятно, как структурировать асинхронный код. В этом уроке мы разберём некоторые полезные практики, делающие его проще для написания и анализа. Возьмём уже знакомую нам задачку по объединению двух файлов.
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 fsp from 'fs/promises';
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 fsp from 'fs/promises';
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 fsp from 'fs/promises';
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 fsp from 'fs/promises';
// Чего здесь не хватает?
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, 'utf-8').then((data) => contents.concat(data)));
return newAcc;
}, initPromise);
// Если надо, продолжаем обработку
promise.then((contents) => /* обрабатываем все данные полученные из файлов */);
Вам ответят команда поддержки Хекслета или другие студенты.
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
Наши выпускники работают в компаниях:
Зарегистрируйтесь или войдите в свой аккаунт