При всех его преимуществах, асинхронный код очень сложен в анализе. Буквально несколько вложенных колбеков с параллельным выполнением операций — и все, уже практически невозможно разобраться в происходящем. Страшно представить себе большую программу, в которой асинхронно все. Тысяча-другая строк кода — и ни один человек не сможет понять его.
С самого начала разработчики понимали ограниченность подхода с колбеками, но понадобилось немало времени до того, как в JavaScript появилась альтернатива – Promises. Промисы меняют способ организации кода, не добавляя нового синтаксиса. При правильном использовании они позволяют "выпрямить" асинхронный код и сделать его предельно плоским и последовательным.
Большая часть современного JavaScript кода пишется на промисах, а колбеки уходят в прошлое. Например, разработчики Node.js внедрили промисы практически во все встроенные модули. Функции для работы с файловой системой, построенные на промисах, доступны через свойство promises
модуля fs. Сравните примеры:
import fs from 'fs';
// Код на колбеках похож на лесенку
fs.readFile('./first', 'utf-8', (_error1, data1) => {
console.log(data1);
fs.readFile('./second', 'utf-8', (_error2, data2) => {
console.log(data2);
fs.readFile('./third', 'utf-8', (_error3, data3) => {
console.log(data3);
});
});
});
// Код на промисах практически плоский
// Переименование свойства promises в fsp для краткости
const { promises: 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)?
А что, если колбек-функция вернет не просто значение, а промис? Тогда параметром следующего then()
становится результат выполнения этого промиса. Иначе промисы были бы бессмысленны. Пример:
const promise = /* тут какая-то операция */
// Из колбека возвращается промис
.then(() => fsp.readFile(filePath))
// В колбек попадает результат выполнения предыдущего промиса
.then((text) => console.log(text));
Использование промисов внутри любой функции автоматически делает эту функцию асинхронной. Она больше не может вызываться как обычная синхронная функция, так как в таком случае невозможно воспользоваться результатом ее работы, дождаться выполнения операции или узнать об ошибках:
// Неправильное определение
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((data3) => {
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));
Вам ответят команда поддержки Хекслета или другие студенты.
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
Наши выпускники работают в компаниях:
Зарегистрируйтесь или войдите в свой аккаунт