Писать асинхронный код — не самое приятное удовольствие в жизни, и вообще непонятно, как можно создавать достаточно большие асинхронные программы на колбеках. Оказывается, их можно создавать вполне успешно, если использовать не колбеки, а другие механизмы, позволяющие писать асинхронный код без "лесенки". К таким механизмам относятся промисы (Promise). Промисы входят в стандарт EcmaScript и реализованы практически во всех рантаймах. Node.js постепенно интегрирует их во все свои модули: например, в модуле fs промисы доступны как свойство promises. У промисов довольно много особенностей, и к ним нужно привыкнуть. Этим мы и займёмся на протяжении ближайших уроков.

import { promises as fs } from 'fs';

export const copy = (src, dest) => {
  return fs.readFile(src, 'utf-8')
    .then(content => fs.writeFile(dest, content));
};
// Тот же код в более короткой записи
// const copy = (src, dest) =>
//   fs.readFile(src, 'utf-8').then(content => fs.writeFile(dest, content));

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

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

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

const promise = fs.readFile(src, 'utf-8');
console.log(promise);
// Promise { <pending> }

Теперь самое интересное: как получить результат выполнения асинхронной операции? Снаружи — никак, это просто невозможно. Но промис можно "продолжить", используя метод then, в который нужно передать функцию обработчик — такой же колбек, который мы использовали ранее. Отличие этого колбека в том, что он принимает на вход только данные, а не ошибки. Обработка ошибок в промисах рассматривается в следующем уроке.

  fs.readFile(src, 'utf-8').then(content => console.log(content));
  // Или проще, ведь функции — уже функции, их не надо оборачивать в функции
  // fs.readFile(src, 'utf-8').then(console.log);

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

const promise = fs.readFile(src, 'utf-8') // результат цепочки ВСЕГДА промис
  .then(() => 'go to the next then') // игнорируем результат операции
  .then(console.log); // в этот колбек, роль которого играет лог, передается значение с предыдущего then
// => go to the next then

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

Если в коде начали применяться промисы, то весь асинхронный код должен строиться с их использованием. Иногда бывает такое, что нужная библиотека умеет работать только с колбеками, и тогда её оборачивают в промис. То, как это сделать, мы рассмотрим позже, когда научимся использовать промисы.

В нашем первом примере демонстрировался код, в котором из then возвращается промис.

fs.readFile(src, 'utf-8').then(content => fs.writeFile(dest, content));

В этом случае промис ведёт себя немного отлично от возврата обычного значения. Дальнейшая цепочка начинает строиться от того промиса, который вернулся из колбека.

fs.readFile(src, 'utf-8')
  .then(content => fs.writeFile(dest, content))
  // Следующий then берется от writeFile. То есть этот код равносилен fs.writeFile(dest, content).then(...)
  .then(() => console.log('writing has been finished!'));

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

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

Если бы возврата не было, то было бы непонятно, как получить результат копирования или хотя бы дождаться его завершения.


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

  1. promisejs.org
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →