Обещания jQuery 2 подобны сварливым старым бабушкам и дедушкам, которых ваши родители боятся подпускать к детям. Конечно, они могли быть великими (даже первопроходцами!) в свое время, но времена изменились, и они не могут идти в ногу со временем.
(под "детскими" я подразумеваю любую разумную современную среду JavaScript)
Проблема: "Promises" в jQuery 2 не соответствуют требованиям "A+" (в jQuery 3 соответствуют). Моя команда на работе обновляет jQuery 2 до jQuery 3, и это занимает некоторое время. Мы застряли с 2 на данный момент. В то же время нам очень хотелось бы использовать новые разработки на основе Promise в JavaScript, такие как async/await
.
Благословение: мы работаем с транспилером (TypeScript), который преобразует блестящие новые функции в простой старый JavaScript. Давайте посмотрим, как это делается с помощью async/await
и как мы можем взломать его, чтобы он работал на нас.
Спойлер: см. результирующий репозиторий на GitHub…
(Re-)Intro to async/await
Взгляните на этот краткий пример кода на typescriptlang.org/playground:
const test = async () => await Promise.resolve();
Это очень много выведенного кода! Прежде чем мы погрузимся с головой в это, мы должны сначала вспомнить, как работает async/await
.
По сути, пометка функции как «асинхронной» указывает на то, что функция является генератором (функция, которая постоянно останавливается и запускается, с промежуточными возвращаемыми значениями), которая в основном возвращает Promise
s. Сами генераторы концептуально просты:
function* getSomeValues() { yield "foo"; yield "bar"; } // Logs "foo", then "bar" for (const value of getSomeValues()) { console.log(value); }
Обычные читатели видят, что getSomeValues()
возвращает "foo"
, а затем возвращает "bar"
. Проницательные специалисты по генераторам поймут, что на самом деле это машина состояний, которая достигает return "foo";
при итерации = 0 и return "bar";
при итерации = 1.
Вы можете думать, что этот код работает аналогично:
function getSomeValues() { let timeCalled = -1; function next() { timeCalled += 1; switch (timeCalled) { case 0: return { value: "foo" }; case 1: return { value: "bar" }; default: return { done: true }; } } return { next }; } // Logs "foo", then "bar" const iterator = getSomeValues(); while (true) { const { done, value } = iterator.next(); if (done) { break; } console.log(value); }
async/await
строится на вершине этой причудливости генератора, предоставляя специальный синтаксис для генераторов, которые дают Promise
s. «ожидание» «асинхронного» генератора — это сокращение от того, что вы хотите зафиксировать результат этого Promise
локально, .then
продолжите работу с функцией.
См. эту статью на эту тему в JavaScript. Эта статья о том, как это реализовано в Python 3.5 — отличная статья, если вам все еще любопытно.
В любом случае, вернемся к выводу TypeScript, есть три раздела для изучения:
__awaiter
__generator
- Исходный код
1. __awaiter
Этот раздел кода принимает функцию, генерирующую Promise
, async
, и выдает Promise
в качестве конечного результата. Приведенные аргументы важны:
function (thisArg, _arguments, P, generator) {
thisArg
— это родительская область. Неинтересно.
_arguments
будет передано функцииawait
ed. Неинтересно.P
— это классPromise
, который мы будем использовать. Позже мы увидим, что по умолчанию используется глобальныйPromise
через(P || P = Promise)
.generator
— этоasync
хронический генератор (техническиIterable
), который пытаетсяawait
что-то сделать. Предположительно это должно датьPromise
s.
Функция, переданная в P
, принимает resolve, reject
и создает три функции перед запуском generator
:
fulfilled
запускается каждый раз, когдаgenerator
разрешаетvalue
. Он снова пытается запуститься, используяstep(generator.next(value))
внутриtry/catch
, и если это не удается, вызываетreject
. В меру неинтересно.rejected
запускается каждый раз, когдаgenerator
дает отказvalue
. Он пытается получить логику броска генератора, используяstep(generator["throw"](value))
внутриtry/catch
, и если это не удается, вызываетreject
. В меру неинтересно.step
это место, где происходит волшебство. Он пытается получить следующее значение изgenerator
, используя новуюP
Promise
и две предыдущие функции, пока не будет получен результат.done
, после чего он вызываетresolve
.
Расширяем step
и немного украшаем его:
function step(result) { if (result.done) { resolve(result.value); return; } return new Promise(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
Это хорошо.
resolve(result.value)
— довольно сексуальный фрагмент кода. result.value
— это то, что мы получаем каждый раз, когда код асинхронного генератора (наша функция async
) дает результат. Он появляется как когда функция .done
, так и когда она еще выполняется.
2. __генератор
tl;dr
3. Оригинальный код
…в __waiter-ified форме. _a.label
, который он включает, указывает на то, какая итерация конечного автомата/цикла генератора выполняется. [number, any]
имеет удобную метку /* commented */
для того, какая операция происходит в этом итерационном цикле, за которой следует выполняемая операция.
Возьмем простую примерную функцию:
async function example() { // result.value will be promise const promise = Promise.resolve("foo"); await promise; // result.value will be "foo" return "foo"; }
Вот как он выглядит преобразившись на детской площадке:
function example() { return __awaiter(this, void 0, void 0, function () { var promise; return __generator(this, function (_a) { switch (_a.label) { case 0: promise = Promise.resolve("foo"); return [4 /*yield*/, promise]; case 1: _a.sent(); // result.value will be "foo" return [2 /*return*/, "foo"]; } }); }); }
К этому моменту у вас должно быть смутное представление о том, как исходный код преобразуется из функции async
в генератор.
(Интересно узнать об отсутствующем комментарии? См. Ошибка TypeScript № 15323.)
Использование промисов jQuery 2 вместо промисов
Наша первая задача, чтобы подчинить этот беспорядок нашей воле, должна состоять в том, чтобы заставить его перестать принимать глобальное Promise
и вместо этого начать использовать JQueryPromise
из jQuery 2.
__awaiter
TypeScript начинается с логики, чтобы не переопределять существующие __awaiter
: var __awaiter = (this && this.__awaiter) || …
. Если мы определим свой собственный, он переопределит все TypeScript.
// No existence check here! var __awaiter = function ...
Вместо P
и new (P || (P = Promise))
давайте определим jQueryPromiseFactory
и используем его в нашем собственном custom__awaiter
.
function jQueryPromiseFactory(callback) { var deferred = $.Deferred(); var promise = deferred.promise(); try { callback(deferred.resolve, deferred.reject); } catch (error) { deferred.reject(error); } return promise; }
Используя его вместо переданного P
в __awaiter
для возвращаемого P
…
var __awaiter = function (thisArg, _arguments, _ignore, generator) { return jQueryPromiseFactory(...); };
…и step
P
позже…
function step(result) { if (result.done) { resolve(result.value); return; } return jQueryPromiseFactory(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
Очень круто. Теперь мы обманули TypeScript, заставив его использовать $.Deferred().promise()
вместо глобального Promise
.
Получение ожидаемых значений
Прямо сейчас, вместо того, чтобы возвращать ожидаемое значение, когда мы await
, мы получаем Object (state, always, ...)
. Это Object
— это JQueryPromise
, полученное от генератора вместо фактического значения. Вместо этого нам придется вернуть его разрешенное значение.
Для спокойствия вот очищенная версия тела __awaiter
:
return jQueryPromiseFactory(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : jQueryPromiseFactory(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])) .next()); });
result
— это то, что нам дал генератор. result.done
— это завершение работы генератора (мы должны resolve
закончить), а result.value
— необработанное значение того, что было возвращено нам (выше сначала promise
, затем "bar"
).
Поскольку мы знаем, что result.value
иногда будет JQueryPromise
сейчас, а step
— это то, что асинхронно ожидает следующей итерации, именно здесь мы должны вставить нашу логику ожидания. Изменение его, чтобы уважать наши пути:
function step(result) { // Part 1 if (!result.value || !result.value.then) { resolve(result.value); return; } // Part 2 result.value .then(function (resolvedValue) { fulfilled(resolvedValue); }) .fail(function (rejectedError) { rejected(rejectedError); }); }
Part 1
проверяет регистр выполняемой функции, но делает это, видя, является ли результат .then
-способным.
Part 2
теперь предполагает, что result
является JQueryPromise
, и использует методы jQuery .then
и .fail
для вызова fulfilled
или rejected
по мере необходимости. Обратите внимание, что в jQuery 2 нет метода .catch
.
return jQueryPromiseFactory(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch(e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch(e) { reject(e); } } function step(result) { // Part 1 if (!result.value || !result.value.then) { resolve(result.value); return; } // Part 2 result.value .then(function (resolvedValue) { fulfilled(resolvedValue); }) .fail(function (rejectedError) { rejected(rejectedError); }); } step((generator = generator.apply(thisArg, _arguments || [])) .next()); });
Предостережения
Вам следует перейти на jQuery 3. Он содержит промисы, соответствующие стандартам. Эту библиотеку следует использовать только в качестве полифилла, пока ваша команда работает над обновлением.
.then
в jQuery 2 по умолчанию является синхронным, но может быть и асинхронным. Реализации Promise, соответствующие стандартам, всегда асинхронны. Не структурируйте свой код, предполагая, что обратные вызовы .then
выполняются синхронно.
Поскольку .then
является синхронным, если ошибка выдается синхронно в JQueryPromise
, любой последующий код (включая await
s) не будет выполняться. Это означает, что вы не можете использовать await
в блоках try
на этом адаптере. Вместо этого поместите рискованную логику в функцию, отличную от async
.
В заключении
Используйте это на свой страх и риск. Честно говоря, я не знаю, какие ужасные вещи произойдут, когда вы попробуете это. Пока что мы используем его только в тестовом коде, потому что смертельно боимся появления невиданных ранее ошибок в рабочей среде.
- github.com/joshuakgoldberg/jquery-2-typescript-async-await-adapter
npm install jquery-2-typescript-async-await-adapter
Если вам действительно интересно, как TypeScript делает это, их исходный код генераторов.ts — отличная вещь для изучения.