В этом руководстве наглядно объясняем идею промисов на примере работы 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
«выпрямил» его. Благодаря ей нет необходимости делать множество вложенных вызовов с отступами: код выглядит как прямая цепочка вызовов. Эта идея напрямую нас подводит к тому, как работают промисы.
Никогда не останавливайтесь: В программировании говорят, что нужно постоянно учиться даже для того, чтобы просто находиться на месте. Развивайтесь с нами — на Хекслете есть сотни курсов по разработке на разных языках и технологиях