Скидки до 20% + 2-ая профессия бесплатно и подарки на 50 000₽

Главная | Все статьи | Код

Waterfall — от колбеков к промисам

Время чтения статьи ~6 минут 35
Waterfall — от колбеков к промисам главное изображение

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

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

Предположим, что у нас есть три функции. getUsers() возвращает список пользователей, getTasks() — список задач, а tasksDist() распределяет все задачи между пользователями, возвращая в качестве результата тот же список задач, но уже с указанными исполнителями.

В обычном (не асинхронном) коде можно вызывать функции на каждой строке и сразу получить результат:

const users = getUsers();
console.log(users); // => [{ login: 'first@google.com', id: 1, ... }, { login: 'second@google.com', id: 2, ... }]
const tasks = getTasks();
console.log(tasks); // => [{ name: 'Написать статью по асинхронному коду', id: 1, ... }, { name: 'Исправить баги в 2 проекте', id: 2, ... }]
const result = tasksDist(users, tasks);
console.log(result); // => [{ name: 'Написать статью по асинхронному коду', id: 1, assigned: 1 ... }, { name: 'Исправить баги в 2 проекте', id: 2, assigned: 2 ... }]

Если мысленно представить порядок работы такого кода, то можно выстроить такую структуру:

Теперь предположим, что функции getUsers(), getTasks() и tasksDist() асинхронные, при этом никакие изменения в код не вносятся. В этом случае порядок их выполнения меняется: каждый вызов асинхронной функции переносит выполнение кода в этой функции в «отдельную временную ветку». В этом основная сложность понимания для новичков: код начинает работать совершенно не так, как раньше. Мы как Нео, проснувшийся из матрицы, обнаруживаем совершенно другой мир, скрывавшийся в привычных нам вещах. Давайте представим, как изменения будут выглядеть на нашей схеме:

Обратите внимание, что каждый вызов асинхронной функции выполняется в своем скоупе. И каждый такой вызов может работать сколько угодно времени: нельзя заранее сказать, что при таком вызове, например, функция getUsers() выполнится и вернет результат до того, как начнет выполнение tasksDist():

Для удобства я выделил выполнение каждой функции цветом и добавил направляющую времени.

Несмотря на то, что функция getUsers() вызвалась самой первой, она выполняется последней. При этом ее результат нужен для выполнения tasksDist(). Хаос вносит и синхронный код, к которому мы так привыкли. В этом примере функции console.log() выполняются по порядку, но не дожидаясь выполнения асинхронных функций.

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

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

Также полезно: Что такое callback-функция в JavaScript?

Хорошая новость: нам не нужно придумывать сложные решения с высчитыванием времени паузы или чем-то в этом роде. Самый простой способ — передать функцию tasksDist() в качестве колбека. Сложность только в том, что мы не знаем, какая из функций getUsers()/ getTasks() выполнится раньше, а какая — позже. Другими словами, непонятно, в какую из них нужно передать колбек. Это решаемый вопрос, но у колбэков есть и другой недостаток — они попросту неудобны. По этому поводу даже существует отдельное понятие —Callback Hell. Ради упрощения в нашем примере сделаем так, чтобы сначала выполнился getUsers(), после него getTasks(), а в конце — tasksDist():

getUsers((users) => {
  console.log(users);
  getTasks((tasks) => {
    console.log(tasks);
    tasksDist(users, tasks, (result) => {
      console.log(result);
    });
  });
});

На схеме результат можно представить в таком виде:

Этот вариант больше подходит для работы: все функции в коде выполняются последовательно друг за другом. Добавим в нашу функцию вызов колбека:

getUsers((users, callback) => {
  console.log(users);
  getTasks((tasks) => {
    console.log(tasks);
    tasksDist(users, tasks, (result) => {
      console.log(result);
      callback(result);
    });
  });
});

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

Читайте также: Как сохранять фокус на протяжении всего обучения: советы от Хекслета

Добавим эту функцию в код:

async.waterfall([
  (callback) => {
    getUsers((users) => callback(users));
  },
  (users, callback) => {
    console.log(users);
    getTasks((tasks) => callback(users, tasks));
  },
  (users, tasks, callback) => {
    console.log(tasks);
    tasksDist((result) => callback(null, result));
  },
], (err, result) => {
  console.log(result);
});

В качестве первого параметра принимается массив функций. Каждая из них, в свою очередь, принимает параметр callback. Например:

(callback) => {
  getUsers(callback);
},

Этот колбек — следующая функция в списке:

(users, callback) => {
  console.log(users);
  getTasks((tasks) => callback(users, tasks));
},

Обратите внимание, что эта функция уже принимает два параметра: users и callback — это скрытая работа waterfall. Кроме самого результата, который передается в предыдущей функции из массива callback(users), добавился новый (callback), который будет следующей функцией в списке:

(users, tasks, callback) => {
  console.log(tasks);
  tasksDist((result) => callback(null, result));
},

Заметьте, что во втором вызове я передал в колбек два значения: callback(users, tasks). Это сделано для того, чтобы в третьей функции не потерялись данные из первой. Альтерантивный вариант: создать глобальную переменную let и в ней сохранить данные.

Последняя функция в массиве тоже принимает колбек, вместо которого подставится последняя функция. Ее waterfall должен отличать от остальных: в ней не должно быть callback. Поэтому эта функция передаётся не внутри массива, а отдельным параметром:

(err, result) => {
  console.log(result);
}

Наш код остался на колбеках, но waterfall «выпрямил» его. Благодаря ей нет необходимости делать множество вложенных вызовов с отступами: код выглядит как прямая цепочка вызовов. Эта идея напрямую нас подводит к тому, как работают промисы.

Никогда не останавливайтесь: В программировании говорят, что нужно постоянно учиться даже для того, чтобы просто находиться на месте. Развивайтесь с нами — на Хекслете есть сотни курсов по разработке на разных языках и технологиях

Аватар пользователя Ivan Gagarinov
Ivan Gagarinov 18 февраля 2022
35
Похожие статьи
Рекомендуемые программы
профессия
Верстка на HTML5 и CSS3, Программирование на JavaScript в браузере, разработка клиентских приложений используя React
10 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на Python, Разработка веб-приложений и сервисов используя Django, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
Тестирование веб-приложений, чек-листы и тест-кейсы, этапы тестирования, DevTools, Postman, SQL, Git, HTTP/HTTPS, API
4 месяца
с нуля
Старт 26 декабря
профессия
Программирование на Java, Разработка веб-приложений и микросервисов используя Spring Boot, проектирование REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
новый
Google таблицы, SQL, Python, Superset, Tableau, Pandas, визуализация данных, Anaconda, Jupyter Notebook, A/B-тесты, ROI
9 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на PHP, Разработка веб-приложений и сервисов используя Laravel, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на Ruby, Разработка веб-приложений и сервисов используя Rails, проектирование и реализация REST API
5 месяцев
c опытом
Старт 26 декабря
профессия
Программирование на JavaScript в браузере и на сервере (Node.js), разработка бекендов на Fastify и фронтенда на React
16 месяцев
с нуля
Старт 26 декабря
профессия
Программирование на JavaScript, разработка веб-приложений, bff и сервисов используя Fastify, проектирование REST API
10 месяцев
с нуля
Старт 26 декабря
профессия
новый
Git, JavaScript, Playwright, бэкенд-тесты, юнит-тесты, API-тесты, UI-тесты, Github Actions, HTTP/HTTPS, API, Docker, SQL
8 месяцев
c опытом
Старт 26 декабря