Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Упорядочивание асинхронных операций JS: Асинхронное программирование

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

import fs from 'fs';

fs.readFile('./first', 'utf-8', '?');
fs.readFile('./second', 'utf-8', '?');
fs.writeFile('./new-file', content, '?');

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

import fs from 'fs';

fs.readFile('./first', 'utf-8', (_error1, data1) => {
  fs.readFile('./second', 'utf-8', (_error2, data2) => {
    fs.writeFile('./new-file', `${data1}${data2}`, (_error3) => {
      console.log('File has been written');
    });
  });
});

В реальных программах количество операций может быть значительно больше: например, десятки — и тогда у вас получится лесенка из 10-ти вложенных вызовов. Подобное свойство асинхронного кода нередко называют Callback Hell («ад колбеков») из-за большого числа вложенных колбеков, которые очень затрудняют анализ программы. Кто-то даже сделал сайт http://callbackhell.com/ , на котором разбирается эта проблема и приводится вот такой код:

import fs from 'fs';

// В этом коде происходит обработка ошибок, которую мы рассмотрим в следующем уроке
fs.readdir(source, (err, files) => {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach((filename, fileIndex) => {
      console.log(filename)
      gm(source + filename).size((err, values) => {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach((width, widthIndex) => {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, (err) => {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

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

import path from 'path';
import fs from 'fs';

const getFileOwners = (dirpath) => {
  // Читаем содержимое директории
  const files = fs.readdirSync(dirpath);
  // Получаем информацию по каждому файлу и формируем результат
  return files
    .map((fname) => [fname, fs.statSync(path.join(dirpath, fname))])
    .map(([fname, stat]) => ({ filename: fname, owner: stat.uid }));
};
// [ { filename: 'Makefile', owner: 65534 },
//       { filename: '__tests__', owner: 65534 },
//       { filename: 'babel.config.js', owner: 65534 },
//       { filename: 'info.js', owner: 65534 },
//       { filename: 'package.json', owner: 65534 } ]

Последовательный код прост и понятен, каждая следующая строчка выполняется после того, как закончится предыдущая, а в map каждый элемент обрабатывается гарантированно последовательно.

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

import path from 'path';
import fs from 'fs';

const getFileOwners = (dirpath, cb) => {
  fs.readdir(dirpath, (_error1, filenames) => {
    const readFileStat = (items, result = []) => {
      if (items.length === 0) {
        // Обработку ошибок пока не рассматриваем
        cb(null, result);
        return;
      }
      const [first, ...rest] = items;
      const filepath = path.join(dirpath, first);
      fs.stat(filepath, (_error2, stat) => {
        readFileStat(rest, [...result, { filename: first, owner: stat.uid }]);
      });
    };
    readFileStat(filenames);
  });
};

Общий принцип такой: формируется специальная функция (readFileStat), которая рекурсивно вызывается, передавая себя в функцию stat. С каждым новым вызовом она отрабатывает один файл и уменьшает массив items, в котором содержатся еще необработанные файлы. Вторым параметром она аккумулирует (собирает) получившийся результат, который в конце передаётся в колбек cb (переданный вторым аргументом функции getFileOwners). Пример выше реализует итеративный процесс, построенный на рекурсивных функциях. Чтобы лучше понять код выше, попробуйте скопировать его к себе на компьютер и позапускайте с разными аргументами, предварительно расставив отладочный вывод внутри неё.


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

  1. Итеративный процесс

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 8 июня
профессия
от 6 300 ₽ в месяц
Разработка бэкенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 8 июня
профессия
от 10 080 ₽ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 8 июня
профессия
от 6 300 ₽ в месяц
новый
Автоматизированное тестирование веб-приложений на JavaScript
10 месяцев
с нуля
в разработке
дата определяется

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

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

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»