company_banner

Упрощение асинхронного кода на JavaScript с внедрением асинхронных функций из ES2016

Автор оригинала: Etienne Baudoux, Brian Terlson
  • Перевод
Хотя мы еще продолжаем работу над внедрением поддержки ES6/2015, команда Chackra также смотрит за пределы ES2016 и, в частности, на асинхронные функции. Мы рады объявить об экспериментальной поддержке async-функций в Microsoft Edge, начиная со сборки Microsoft Edge (EdgeHTML 13.10547).



Асинхронные функции в ES7/ES2016


Ключевые слова async и await, как часть предложения по внедрению асинхронных функций, нацелены на упрощение написания асинхронного кода. Это одна из ключевых возможностей современного C# и часто запрашиваемая опция со стороны JavaScript-разработчиков. До введения асинхронных функций и промисов (promise) JS-разработчику приходилось оборачивать весь асинхронный код в отдельные от синхронного кода функции и использовать функции обратного вызова (callback) для работы с результатом асинхронного вычисления. Такой код довольно быстро становится трудно читать и поддерживать.

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

Асинхронные функции базируются на промисах и позволяют сделать следующий шаг. Когда вы добавляете ключевое слово async к функции или стрелочной функции, она автоматически будет возвращать промис. К примеру, следующий код – это типовая программа на ES2015, делающая http-запрос с помощью промисов:

// ES6 code, without async/await 
function httpGet(url) {
    return new Promise(function (resolve, reject) {
        // do the usual Http request
        var request = new XMLHttpRequest();
        request.open('GET', url);

        request.onload = function () {
            if (request.status == 200) {
                resolve(request.response);
            } else {
                reject(Error(request.statusText));
            }
        };

        request.onerror = function () {
            reject(Error('Network Error'));
        };

        request.send();
    });
}

function httpGetJson(url) {
    return new Promise(function (resolve, reject) {
        // check if the URL looks like a JSON file and call httpGet.
        var regex = /\.(json)$/i;

        if (regex.test(url)) {
            // call the promise, wait for the result
            resolve(httpGet(url).then(function (response) {
                return response;
            }, function (error) {
                reject(error);
            }));
        } else {
            reject(Error('Bad File Format'));
        }
    });
}

httpGetJson('file.json').then(function (response) {
    console.log(response);
}).catch(function (error) {
    console.log(error);
});


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

// ES7 code, with async/await
function httpGet(url) {
    return new Promise(function (resolve, reject) {
        // do the usual Http request
        let request = new XMLHttpRequest();
        request.open('GET', url);

        request.onload = function () {
            if (request.status == 200) {
                resolve(request.response);
            } else {
                reject(Error(request.statusText));
            }
        };

        request.onerror = function () {
            reject(Error('Network Error'));
        };

        request.send();
    });
}

async function httpGetJson(url) {
    // check if the URL looks like a JSON file and call httpGet.
    let regex = /\.(json)$/i;

    if (regex.test(url)) {
        // call the async function, wait for the result
        return await httpGet(url);
    } else {
        throw Error('Bad Url Format');
    }
}

httpGetJson('file.json').then(function (response) {
    console.log(response);
}).catch(function (error) {
    console.log(error);
});


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

// call the async function, wait for the result
let func = async () => await httpGet(url);
return await func();


Итого:
  • Используйте ключевое слово async при определении любой функции или стрелочной функции, чтобы получить асинхронный код с промисом. Это включает также функции в классах и статичные функции. В последнем случае ключевое слово async нужно указывать после слова static и, соответственно, перед именем функции.
  • Используйте ключевое слово await, чтобы ход выполнения дождался завершения async-выражения (к примеру, вызова async-функции) и получило значение из промиса.
  • Если вы не используете ключевое слово await, вы получите сам промис.
  • Вы не можете использовать ключевое слово await вне async-функции, в том числе его нельзя использовать в глобальном пространстве.


Как это реализовано в Chakra?


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



Глобальное поведение


Использование ключевого слова async генерирует конструктор промиса, который, в соответствии со спецификацией, является оберткой вокруг содержимого функции. Для выполнения этого действия, генератор байт-кода Chakra формирует вызов встроенной функции, реализующей следующее поведение:

function spawn(genF, self) {
    return new Promise(function (resolve, reject) {
        var gen = genF.call(self);
        function step(nextF) {
            var next;
            try {
                next = nextF();
            } catch (e) {
                // finished with failure, reject the promise
                reject(e);
                return;
            }
            if (next.done) {
                // finished with success, resolve the promise
                resolve(next.value);
                return;
            }
            // not finished, chain off the yielded promise and `step` again
            Promise.resolve(next.value).then(function (v) {
                step(function () { return gen.next(v); });
            }, function (e) {
                step(function () { return gen.throw(e); });
            });
        }
        step(function () { return gen.next(undefined); });
    });
}


Порождающая функция, приведенная выше, спроектирована так, чтобы обработать все async-выражения в теле функции и решить, нужно ли продолжить или остановить процесс в зависимости от поведения внутри async-функции. Если async-выражение, вызванное с ключевым словом await, проваливается, например, вследствие возникновения ошибки внутри async-функции или в целом ожидаемого выражения, то промис возвращает отказ, который можно обработать выше по стеку.

Далее движок должен вызвать порождающую функцию из JS-скрипта, чтобы получить промис и выполнить содержимое функции. Чтобы это сделать, когда парсер находит ключевое слово async, движок изменяет AST (абстрактное синтаксическое дерево), представляющее алгоритм, чтобы добавить вызов spawn-функции с телом целевой функции. Как следствие, функция httpGetJson из примера выше конвертируется парсером примерно следующим образом:

function httpGetJson(url) {
    return spawn(function* () {
        // check if the URL looks like a JSON file and call httpGet.
        var regex = /\.(json)$/i;

        if (regex.test(url)) {
            // call the async function, wait for the result
            return yield httpGet(url);
        } else {
            throw Error('Bad Url Format');
        }
    }, this);
}


Обратите внимание на использование генераторов и ключевого слова yield для реализации поведения ключевого слова await. На самом деле, реализация поддержки ключевого слова await очень похожа на работу с ключевым словом yield.

Поведение с аргументом по умолчанию


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

// original JavaScript code
function foo(argument = true) {
    // some stuff
}

// representation of the Bytecode Generator's output in JavaScript
function foo(argument) {
    argument = true;
    // some stuff
}


В случае использования ключевого слова async, если значение по умолчанию приводит к возникновению ошибки (исключения), спецификация требует отказать в выполнении промиса. Это позволяет с легкостью отлавливать исключения.

Чтобы реализовать это в Chakra, у команды был выбор из двух вариантов: изменить AST, или реализовать такое поведение напрямую в генераторе байт-кода. Мы выбрали второе и передвинули инициализацию аргументов в начало тела функции напрямую в байт-коде, так как это более простое и понятное решение в рамках нашего движка. Так как для перехвата ошибок из значения по умолчанию нужно было добавить блок try/catch, то нам было проще напрямую изменить байт-код при обнаружении ключевого слова async.

Наконец, сгенерированный байт-код будет напоминать результат, создаваемый для такого кода на JavaScript:

// representation of the Bytecode Generator's output in JavaScript
function foo(argument) {
    try {
        argument = true;
    } catch (error) {
        return Promise.reject(error);
    }
    return spawn(function* () { // keep this call as we are in an async function
        // some stuff
    }, this);
}


Как включить поддержку асинхронных функций Microsoft Edge?


Чтобы включить экспериментальную поддержку асинхронных функций в Microsoft Edge, перейдите на страницу about:flags в Microsoft Edge и выберите опцию “Enable experimental JavaScript features”, как показано ниже:



Асинхронные функции доступны в превью-режиме в рамках программы Windows Insider, начиная со сборки Microsoft Edge 13.10547. Будем рады услышать ваши отзывы по использованию данной функциональности в вашем коде в нашем Twitter @MSEdgeDev или через Connect.

Etienne Baudoux, Software Development Engineer Intern, Chakra Team
Brian Terlson, Senior Program Manager, Chakra Team
Microsoft
210,13
Microsoft — мировой лидер в области ПО и ИТ-услуг
Поделиться публикацией

Комментарии 18

    –1
    Асинхронные функции — это самое крутое нововведение ECMAScript.next. Я очень удивлен, что компания, создавшая Internet Explorer 6 первой реализовала их. Я польщен.

    image
      +3
      Ну если подумать то таже самая компания создала IE 5 в том числе.
        +11
        Что было плохого в IE6 на момент выхода?
          0
          На момент выхода — ничего. Проблема в том, что они не убирали поддержку браузера до тех пор, пока все разработчики не возненавидели Осла. Сейчас они молодцы, внедляют всякие крутые штуки и фиксят баги достаточно быстро. Еще бы по-быстрее IE 9-11 ушли на пенсию, мир бы стал добрее.
          –4
          Стандарт async-функций еще не утвержден. Если в нем к моменту выхода что-то поменяется, то мы получим два разных async: в нормальных браузерах по стандарту и особое поведение в IE.

          Зачем спешить реализовывать нестабильные фичи, если есть уже сделанные в других браузерах, ServiceWorkers, например.
          • НЛО прилетело и опубликовало эту надпись здесь
              +2
              Текущая стадия предложения асинхронных функций 3 — кандидат — рекомендовано для имплементации в движках для получения обратной связи от имплементаторов и пользователей, но не для использования в рабочих проектах. Кстати, ServiceWorkers тоже пока только на стадии черновика.
            +5
            Пример без async-функции искусственно усугубляет ситуацию. Я бы написал функцию httpGetJson вот так:

            function httpGetJson(url) {
                // check if the URL looks like a JSON file and call httpGet.
                var regex = /\.(json)$/i;
                if (regex.test(url)) {
                    return httpGet(url);
                } else {
                    return Promise.reject(Error('Bad File Format'));
                }
            }
            

            Получилось ровно столько же строк, что и в async-варианте.

            Перед тем, как мечтать о волшебных async-функциях, стоит внимательнее изучить то, что у нас уже есть. Например, прочитав недавнюю статью о правильном использовании Promise.
              –4
              Очень уж много буков в вашей функции:
              const httpGetJson = url => /\.(json)$/i.test(url) ?
                httpGet(url) :
                Promise.reject(Error('Bad File Format'));
              


              Но так, имхо, намного лучше:
              const httpGetJson = async url => /\.(json)$/i.test(url) ?
                await httpGet(url) :
                throw Error('Bad File Format');
              
                +3
                Проблемы начинаются, когда внутри одной функции нужно несколько последовательных асинхронных вызовов. Конечно, можно разбивать все это на разные функции, но это означает, что мы думаем не об архитектуре, а о синтаксических возможностях.
                Также, можно использовать Promise, then, но тогда нужно писать огромное количество шаблонного кода:
                asyncPromise1().then(function(){
                    return asyncPromise2();
                }).then(function(arg2){
                    console.log(arg2);
                });
                

                VS
                await asyncPromise1();
                console.log(await asyncPromise2())
                
                  0
                  asyncPromise1()
                    .then(asyncPromise2)
                    .then(console.log);
                  
                    0
                    К сожалению, ваш пример не рабочий, как минимум нужно записать так
                    asyncPromise1()
                      .then(asyncPromise2)
                      .then(console.log.bind(console));
                    

                    К тому же, ваш вариант просто проглатывает возможные ошибки (нет catch), а async/await вызовут Error.

                    Но для продолжения дискуссии, предлагаю написать вариант еще и для такого кода:

                    await asyncPromise1();
                    console.log(await asyncPromise2() + await asyncPromise3());
                    
                      0
                      Так я просто переписал первый кусок кода с промисами из вашего комментария в более читабельном виде. В node, кстати console.log биндить к console не нужно, но в браузерах, конечно же, не так. Но на самом-то деле я считаю что async/await это очень хорошо и красиво и поскорее бы они были везде.

                      Насчет вашей задачи: я бы сделал как-то так:

                      var asyncPromise1 = () => Promise.resolve();
                      var asyncPromise2 = () => Promise.resolve('foo');
                      var asyncPromise3 = () => Promise.resolve('bar');
                      
                      asyncPromise1()
                        .then(asyncPromise2)
                        .then(arg2 =>
                          asyncPromise3()
                            .then(arg3 => arg2 + arg3))
                        .then(console.log.bind(console))  // "foobar"
                        .catch(err => console.log(err));
                      


                      Читабельность тут, по сравнению с async/await, конечно же, хуже, но я все еще могу понять что здесь происходит.
                        +3
                        Чуть читабельнее:

                        asyncPromise1()
                            .then(_ => Promise.all([asyncPromise2(), asyncPromise3()])
                            .then(([a2, a3]) => console.log(a2 + a3))
                            .catch(err => console.log(err));
                        
                          0
                          да, спасибо. destructuring assignment — это шикарно
                –5
                А планируете что-то сделать с «замечательной» фичей, когда случайно вытаскиваешь вкладку в отдельное окно, перетаскиваешь ее назад, а окно, в которое она выделилась, уже без вкладок, так и висит незакрытым? Раздражает.
                • НЛО прилетело и опубликовало эту надпись здесь
                    –1
                    Охренеть! Ребята, это потрясающе. Искренне жму вам руки.

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

                    Самое читаемое