Ситуация с асинхронным кодом становится резко сложнее, если мы попытаемся выполнить несколько вызовов одновременно и затем воспользоваться их результатом. Попробуем переписать нашу задачу по объединению файлов. Напомню её текст:

Предположим, что перед нами стоит задача прочитать содержимое двух файлов и записать в третий (объединение файлов).

Из постановки видно, что оба исходных файла можно прочитать одновременно и затем, когда они оба будут прочитаны, записать новый файл.

fs.readFile('./first', 'utf-8', (error1, data1) => {
  // ?
});

fs.readFile('./second', 'utf-8', (error2, data2) => {
 // ?
});

Так как наш код асинхронный, результат работы каждой функции можно получить лишь внутри колбеков. Причём, порядок запуска колбеков мы не можем знать — всё зависит от того, какой файл прочитается быстрее. Для отслеживания состояния выполнения этих операций придётся ввести глобальное состояние (относительно этих операций), через которое мы будем отслеживать завершённость и в котором сохраним данные. И только когда все операции завершились — запишем новый файл. Кроме того, нам нужно чётко разделять данные первого и второго файлов, так как запись в новый файл (в отличие от чтения) должна происходить в определённом порядке.

const state = {
  count: 0,
  results: [];
}

fs.readFile('./first', 'utf-8', (error1, data1) => {
  state.count += 1;
  state.results[0] = data1;
});

fs.readFile('./second', 'utf-8', (error2, data2) => {
  state.count += 1;
  state.results[1] = data2;
});

Когда обе операции завершатся, состояние заполнится данными, а значение count станет 2. Именно на это условие мы и завяжем наш код:

import fs from 'fs';

const state = {
  count: 0,
  results: [],
}

const tryWriteNewFile = (error) => {
  if (error) {
    return; // guard expression
  }
  if (state.count !== 2) {
    return; // guard expression
  }

  fs.writeFile('./new-file', state.results.join(''), (error) => {
    if (error) {
      return;
    }
    console.log('finished!');
  });
}

console.log('first reading was started');
fs.readFile('./first', 'utf-8', (error1, data1) => {
  console.log('first callback');
  state.count += 1;
  state.results[0] = data1;
  tryWriteNewFile(error1);
});

console.log('second reading was started');
fs.readFile('./second', 'utf-8', (error2, data2) => {
  console.log('second callback');
  state.count += 1;
  state.results[1] = data2;
  tryWriteNewFile(error2);
});

// Один запуск
// $ node index.js
// first reading was started
// second reading was started
// second callback
// first callback
// finished!

// Другой запуск
// $ node index.js
// first reading was started
// second reading was started
// first callback
// second callback
// finished!

Теперь файлы читаются параллельно и мы, наконец-то, увидели на практике преимущество одновременного выполнения асинхронных операций. Скорость выполнения этой программы значительно выше синхронного варианта! Причём, чем больше размер файлов, тем больше разница. Однако, стоит заметить, что, хотя загрузка файлов происходит параллельно, работа самого js, обрабатывающего результат — строго последовательна. Колбеки начинают запускаться только после того, как опустеет текущий стек вызовов, и в том порядке, в котором завершились асинхронные операции (параллельный запуск не означает, что операции заканчиваются и начинаются одновременно).

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

import { map } from 'async';
import fs from 'fs';

map(['./first', './second'], fs.readFile, (err1, results) => {
  if (err1) {
    return;
  }
  fs.writeFile('./new-file', results.join(''), (err2) => {
    if (err2) {
      return;
    }
    console.log('finished!');
  });
});

Согласитесь, что это значительно лучше ;) Но, как увидите позже, можно пойти ещё дальше.


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

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

Хекслет

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