Как превратить функцию, которая принимает обратный вызов, в обещание

Большинство из нас, разработчиков JavaScript, ненавидят работу с обратными вызовами. Наверняка многие из вас слышали про ад обратных вызовов и то, как он портит читабельность приложения. Если мы пишем наш код, у нас есть возможность использовать обещания, но могут быть встроенные функции или функции из внешних библиотек, которые используют обратные вызовы.

Почему бы тогда не превратить их в обещания?

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

Пример

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

Для запуска функции нам нужно предоставить обратный вызов с двумя аргументами — результатом и потенциальной ошибкой.

Теперь предположим, что вы хотите использовать его в своем красивом async/await коде. Есть два пути, по которым вы можете пойти.

Короткий путь

Лучший и самый простой способ — использовать уже реализованный метод для превращения функции, принимающей обратный вызов, в промис. Если вы используете node.js, для этого подходит метод promisify в пакете utils. На фронтенде можно использовать популярный пакет es6-promisify.

Пример с promisify из utils:

После использования promisify наша функция превращается в обещание. Теперь мы можем использовать на нем .then/.catch или await/try-catch.

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

Но… что происходит под капотом?

Путь вручную

Чтобы лучше понять это, давайте сами пообещаем предоставленную функцию. Давайте начнем с определения того, как мы хотим его назвать. Это будет аналог promisify из utils:

Вот одна из возможных реализаций customPromisify:

Это немного сложно, поэтому давайте разберемся.

  1. line #2: customPromisify вернуть функцию, которой мы передадим аргументы исходной функции. В данном примере параметры 'a' и 'b' будут присутствовать внутри массива args.
  2. line #3: возвращаемая функция возвращает обещание, поэтому мы можем присоединить к нему .then и .catch (или использовать await).
  3. lines #4–7: внутри промиса мы создаем пользовательский обратный вызов, который будет resolve или reject промисом соответственно.
  4. line #8: Затем нам нужно добавить пользовательский обратный вызов в список аргументов, чтобы он передавался функции (fn) и выполнялся при необходимости.
  5. line #9: Наконец, мы вызываем предоставленную функцию (fn) для массива args, содержащего customCallback.

Вот и все! У нас есть способ обещать функцию. customPromisify можно расширить, чтобы принимать больше обратных вызовов или нестандартных обратных вызовов.

Если вы хотите увидеть более индивидуальный пример обещания, вы можете проверить 10-й шаг в Как разработать расширение React Chrome для Medium за 26 шагов.

Теперь вы знаете, как. Давайте ответим, когда это сделать.

Когда безопасно обещать?

Чтобы ответить на этот вопрос, нам нужно понять одно важное различие между обещанием и обратным вызовом.

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

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

Небезопасное обещание

Вы знаете функцию setInterval()? Он принимает пользовательскую функцию в качестве обратного вызова и выполняет ее каждые X миллисекунд. Вот пример:

Он будет печатать "test" каждые 1000ms, пока вы не завершите процесс или не используете clearInterval().

Превратим это в обещание:

customPromisify был немного изменен для этого случая. Обратный вызов теперь принимает только один аргумент. Он также добавляется в качестве первого аргумента в список args, поэтому он соответствует способу реализации setInterval.

Это выполнение приведенного выше кода:

"test" печатается один раз, потому что обещание может быть разрешено только один раз. Более того, setInterval никогда не очищается, поэтому программа зависает.

Заключение

setInterval не лучший кандидат для обещаний. На самом деле, любая функция, которая вызывает свой обратный вызов несколько раз, не должна быть промисифицирована.

Заключительные заметки

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