Promises, Thenables и ленивая оценка: что, почему, как

Это начало нового года, и хотя многие люди обещают быть более активными, я собираюсь показать вам, как сделать Promise более ленивыми… JavaScript Promise, то есть.

Это будет иметь больше смысла через мгновение.

Во-первых, давайте рассмотрим базовый пример Promise. Здесь у меня есть функция, называемая sleep, которая принимает время в миллисекундах и значение. Он возвращает обещание, которое выполнит setTimeout в течение количества миллисекунд, которое мы должны ждать, затем обещание разрешается со значением.

/**
 * @template ValueType
 * @param {number} ms
 * @param {ValueType} value
 * @returns {Promise<ValueType>}
 */
function sleep(ms, value) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}

Это работает следующим образом:

Мы можем дождаться функции sleep с аргументами 1000 и 'Yawn & stretch', и через одну секунду console зарегистрирует строку «Зевок и потягивание».

В этом нет ничего особенного. Вероятно, он ведет себя так, как вы ожидаете, но будет немного странно, если мы сохраним его как переменную в await позже, а не сразу awaiting возвращаемое Promise.

const nap = sleep(1000, 'Yawn & stretch')

Теперь предположим, что мы делаем какую-то другую работу, требующую времени (например, печатаем следующий пример), а затем await переменную nap.

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

В нашем примере, когда мы определяем переменную nap, создается переменная Promise, которая выполняет setTimeout. Поскольку я медленно печатаю, Promise будет решено к тому времени, когда мы его await.

Другими словами, Promises нетерпеливы. Они не ждут, пока вы их await.

В некоторых случаях это хорошо. В других случаях это может привести к ненужному использованию ресурсов. Для этих сценариев вам может понадобиться что-то похожее на Promise, но использующее ленивую оценку для создания экземпляра только тогда, когда вам это нужно.

Прежде чем мы продолжим, я хочу показать вам кое-что интересное.

Promises — не единственное, что может быть awaited в JavaScript. Если мы создадим простой Object с помощью метода .then(), мы фактически сможем await этот объект точно так же, как любой Promise.

Это довольно странно, но также позволяет нам создавать различные объекты, которые выглядят как Promise, но не являются таковыми. Эти объекты иногда называют Thenables.

Имея это в виду, давайте создадим новый класс с именем LazyPromise, который расширяет встроенный конструктор Promise. Расширение Promise не является строго необходимым, но делает его более похожим на Promise, используя такие вещи, как instanceof.

class LazyPromise extends Promise {
  /** @param {ConstructorParameters<PromiseConstructor>[0]} executor */
  constructor(executor) {
    super(executor);
    if (typeof executor !== 'function') {
      throw new TypeError(`LazyPromise executor is not a function`);
    }
    this._executor = executor;
  }
  then() {
    this.promise = this.promise || new Promise(this._executor);
    return this.promise.then.apply(this.promise, arguments);
  }
}

Часть, на которой следует сосредоточиться, — это метод then(). Он перехватывает стандартное поведение стандартного Promise, чтобы дождаться выполнения метода .then() перед созданием реального Promise. Это позволяет избежать создания экземпляра асинхронной функции до тех пор, пока вы ее не вызовете. И это работает независимо от того, вызываете ли вы явно .then() или используете await.

Теперь давайте посмотрим, что произойдет, если мы заменим Promise в исходной функции sleep на LazyPromise. И снова мы присвоим результат переменной nap.

function sleep(ms, value) {
  return new LazyPromise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}
const nap = sleep(1000, 'Yawn & stretch')

Затем мы набираем время, чтобы ввести строку await nap и выполнить ее.

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

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

Конечно, это тривиальный пример, который вы, вероятно, не найдете в рабочем коде, но есть много проектов, в которых используются объекты, подобные Promise, с отложенной оценкой. Вероятно, наиболее распространенным примером является база данных ORM и конструкторы запросов, такие как Knex.js или Prisma.

Рассмотрим псевдокод ниже. Он вдохновлен некоторыми из этих конструкторов запросов:

const query = db('user')
  .select('name')
  .limit(10)
const users = await query

Мы создаем запрос к базе данных, который обращается к таблице "user", выбирает первые десять записей и возвращает их имена. Теоретически это будет работать с обычным Promise.

Но что, если мы хотим изменить запрос на основе определенных условий, таких как параметры строки запроса? Было бы неплохо иметь возможность продолжить изменение запроса, прежде чем в конечном итоге дождаться Promise.

const query = db('user')
  .select('name')
  .limit(10)
if (orderBy) {
  query.orderBy(orderBy)
}
if (limit) {
  query.limit(limit)
}
if (id) {
  query.where({ id: id })
}
const users = await query

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

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

Это один из примеров, когда ленивые вычисления хороши. Это также может быть полезно для таких вещей, как создание, изменение и оркестровка HTTP-запросов.

Ленивые Promise очень хороши для правильных вариантов использования, но это не значит, что они должны заменить все Promise. В некоторых случаях полезно создавать экземпляры с нетерпением и иметь готовый ответ как можно скорее. Это еще один из тех сценариев «это зависит». Но в следующий раз, когда кто-то попросит вас сделать Promise, подумайте о том, чтобы полениться ( ͡° ͜ʖ ͡°).

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

Первоначально опубликовано на austingil.com.