Введение

Многие функции в клиенте JavaScript Diffusion возвращают объект Result. Этот объект можно использовать для получения возвращаемых значений асинхронно выполняемого кода. Result реализует then() метод, который принимает одну или две функции обратного вызова. Первая функция обратного вызова будет вызываться с возвращаемым значением после завершения асинхронного вызова. Вторая функция обратного вызова будет вызвана в случае ошибки.

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

Ловушка обратного вызова

При рассмотрении обработчика then() с точки зрения традиционных функций обратного вызова возникает соблазн рекурсивно обернуть обратные вызовы друг в друга. Это может привести к нечитаемому и потенциально ошибочному коду. Рассмотрим следующий пример. Функция должна последовательно запускать четыре задачи.

  1. подключиться к серверу Diffusion и получить сеанс
  2. создать новую тему
  3. отправить сообщение получателю и получить ответ
  4. установить ранее созданную тему со значением, полученным в ответе на сообщение

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

function callback_pyramid() {
    diffusion.connect({
        principal : '',
        credentials : ''
    }).then(
        function(session) {
            session.topics.add('path/to/a/topic', diffusion.topics.TopicType.STRING).then(
                function() {
                    session.messages.sendRequest('/path/to/message/topic', 'foo').then(
                        function(value) {
                            session.topics.set('path/to/a/topic', diffusion.datatypes.string(), value).then(
                                function(value) {
                                    console.log("topic has been updated to "+value);
                                },
                                // Error callback for session.topics.set
                                function (error) {
                                    console.log(error);
                                }
                            )
                        },
                        // Error callback for session.messages.sendRequest
                        function (error) {
                            console.log(error);
                        }
                    );
                },
                // Error callback for session.topics.add
                function (error) {
                    console.log(error);
                }
            );
        },
        // Error callback for diffusion.connect
        function (error) {
            console.log(error);
        }
    );
}

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

Использование обещаний

Приведенный выше пример можно значительно улучшить. В спецификации Promise A + указано, что then возвращает другой Promise. Если функция, переданная обработчику then(), возвращает значение, Promise будет разрешено с этим значением. Если, с другой стороны, это значение, возвращаемое обработчиком, является Promise, тогда Promise, возвращаемое then(), будет разрешено, когда Promise, возвращенное обработчиком, разрешится. Хотя это звучит сложно, это означает, что можно объединить then() функций в цепочку. Каждый член в цепочке будет выполнен только после того, как завершатся предыдущие участники.

Тот же пример, что и выше, теперь можно записать следующим образом.

function chaining_promises() {
    diffusion.connect({
        principal : '',
        credentials : ''
    }).then(
        function(session) {
            return session.topics.add('path/to/a/topic', diffusion.topics.TopicType.STRING);
        }
    ).then(
        function() {
            return session.messages.sendRequest('/path/to/message/topic', 'foo');
        }
    ).then(
        function(value) {
            return session.topics.set('path/to/a/topic', diffusion.datatypes.string(), value);
        }
    ).then(
        function(setValue) {
            console.log("topic has been updated to "+setValue);
        }
    ).catch (
        // Combined error callback
        function (error) {
            console.log(error);
        }
    );
}

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

Использование async / await

Относительно современной особенностью JavaScript, представленной в ES6, является введение ключевых слов async и await. Функцию можно объявить асинхронной, поставив перед ней префикс async. Это указывает на то, что функция неявно вернет Promise. Внутри асинхронной функции вызовы функций, возвращающих Promises, могут иметь префикс await. Когда функция вызывается, await заставляет выполнение приостанавливаться, пока обещание не разрешится. Это позволяет еще больше упростить приведенный выше пример.

async function using_async_await() {
    try {
        const session = await diffusion.connect({
            principal : '',
            credentials : ''
        });
        await session.topics.add('path/to/a/topic', diffusion.topics.TopicType.STRING);
        const value = await session.messages.sendRequest('/path/to/message/topic', 'foo');
        const setValue = await session.topics.set('path/to/a/topic', diffusion.datatypes.string(), value);
        console.log("topic has been updated to "+setValue);
    }
    // Combined error callback
    catch(error) {
        console.log(error);
    }
}

Обработка ошибок теперь происходит с использованием того, что выглядит как традиционный блок try-catch. Объем стандартного кода сокращается, и операторы представляются в виде, напоминающем синхронное последовательное выполнение. Это приводит к еще более управляемому коду.

Резюме

Использование обещаний в JavaScript может значительно снизить сложность асинхронного кода. За счет замены рекурсивной упаковки обратных вызовов на другую более последовательным подходом улучшается удобочитаемость и ремонтопригодность. Введение async и await еще больше улучшает это. Объект Result, возвращаемый многими функциями в Diffusion, реализует спецификацию Promise A + и естественно вписывается в эту парадигму.

Первоначально опубликовано: https://bit.ly/3fmRkS1