Обещания 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.

По сути, пометка функции как «асинхронной» указывает на то, что функция является генератором (функция, которая постоянно останавливается и запускается, с промежуточными возвращаемыми значениями), которая в основном возвращает Promises. Сами генераторы концептуально просты:

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 строится на вершине этой причудливости генератора, предоставляя специальный синтаксис для генераторов, которые дают Promises. «ожидание» «асинхронного» генератора — это сокращение от того, что вы хотите зафиксировать результат этого Promise локально, .then продолжите работу с функцией.

См. эту статью на эту тему в JavaScript. Эта статья о том, как это реализовано в Python 3.5 — отличная статья, если вам все еще любопытно.

В любом случае, вернемся к выводу TypeScript, есть три раздела для изучения:

  1. __awaiter
  2. __generator
  3. Исходный код

1. __awaiter

Этот раздел кода принимает функцию, генерирующую Promise, async, и выдает Promise в качестве конечного результата. Приведенные аргументы важны:

function (thisArg, _arguments, P, generator) {

thisArg — это родительская область. Неинтересно.

  • _arguments будет передано функции awaited. Неинтересно.
  • P — это класс Promise, который мы будем использовать. Позже мы увидим, что по умолчанию используется глобальный Promise через (P || P = Promise).
  • generator — это asyncхронический генератор (технически Iterable), который пытается await что-то сделать. Предположительно это должно дать Promises.

Функция, переданная в 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, любой последующий код (включая awaits) не будет выполняться. Это означает, что вы не можете использовать await в блоках try на этом адаптере. Вместо этого поместите рискованную логику в функцию, отличную от async.

В заключении

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

Если вам действительно интересно, как TypeScript делает это, их исходный код генераторов.ts — отличная вещь для изучения.