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

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 fs } from 'fs';

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

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

import { promises as fs } from 'fs';

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

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

import { promises as fs } from 'fs';

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

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

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

Хекслет

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