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
позже, а не сразу await
ing возвращаемое Promise
.
const nap = sleep(1000, 'Yawn & stretch')
Теперь предположим, что мы делаем какую-то другую работу, требующую времени (например, печатаем следующий пример), а затем await
переменную nap
.
Вы можете ожидать задержку в одну секунду перед разрешением, но на самом деле оно разрешается немедленно. Каждый раз, когда вы создаете Promise
, вы создаете экземпляр любой асинхронной функциональности, за которую он отвечает.
В нашем примере, когда мы определяем переменную nap
, создается переменная Promise
, которая выполняет setTimeout
. Поскольку я медленно печатаю, Promise
будет решено к тому времени, когда мы его await
.
Другими словами, Promise
s нетерпеливы. Они не ждут, пока вы их await
.
В некоторых случаях это хорошо. В других случаях это может привести к ненужному использованию ресурсов. Для этих сценариев вам может понадобиться что-то похожее на Promise
, но использующее ленивую оценку для создания экземпляра только тогда, когда вам это нужно.
Прежде чем мы продолжим, я хочу показать вам кое-что интересное.
Promise
s — не единственное, что может быть await
ed в 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.