Статьи

Программирование на основе событий: что асинхронно синхронизировано

Сильной стороной JavaScript является то, как он обрабатывает асинхронный (для краткости асинхронный) код. Вместо того, чтобы блокировать поток, асинхронный код помещается в очередь событий, которая запускается после выполнения всего остального кода. Однако новичкам может быть трудно следовать асинхронному коду. Я помогу устранить любую путаницу, которая может возникнуть в этой статье.


Основными асинхронными функциями JavaScript являются setTimeout и setInterval . Функция setTimeout выполняет данную функцию по истечении определенного промежутка времени. Он принимает функцию обратного вызова в качестве первого аргумента и время (в миллисекундах) в качестве второго аргумента. Вот пример его использования:

01
02
03
04
05
06
07
08
09
10
11
console.log( «a» );
setTimeout(function() {
    console.log( «c» )
}, 500 );
setTimeout(function() {
    console.log( «d» )
}, 500 );
setTimeout(function() {
    console.log( «e» )
}, 500 );
console.log( «b» );

Как и ожидалось, консоль выводит «a», «b», а затем через 500 мс (ish) мы видим «c», «d» и «e». Я использую «ish», потому что setTimeout на самом деле непредсказуемо. На самом деле, даже спецификация HTML5 говорит об этой проблеме:

«Этот API не гарантирует, что таймеры будут работать точно по расписанию. Следует ожидать задержек из-за загрузки процессора, других задач и т. Д.»

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

Цикл событий — это очередь функций обратного вызова. Когда выполняется асинхронная функция, функция обратного вызова помещается в очередь. Движок JavaScript не начинает обрабатывать цикл событий до тех пор, пока не выполнится код после асинхронной функции. Это означает, что код JavaScript не является многопоточным, даже если кажется, что это так. Цикл обработки событий представляет собой очередь «первым пришел-первым вышел» (FIFO), что означает, что обратные вызовы выполняются в том порядке, в котором они были добавлены в очередь. JavaScript был выбран для языка узла из-за того, как легко писать такой код.


Асинхронный JavaScript и XML (Ajax) навсегда изменили ландшафт JavaScript. Внезапно, браузер может обновить веб-страницу без перезагрузки. Код для реализации Ajax в разных браузерах может быть длинным и утомительным для написания; однако благодаря jQuery (и другим библиотекам) Ajax стал чрезвычайно простым и элегантным решением для облегчения взаимодействия клиент-сервер.

Асинхронный поиск данных с помощью $.ajax jQuery — это простой кросс-браузерный процесс, но не сразу видно, что именно происходит за кулисами. Например:

01
02
03
04
05
06
07
08
09
10
var data;
$.ajax({
    url: «some/url/1»,
    success: function( data ) {
        // But, this will!
        console.log( data );
    }
})
// Oops, this won’t work…
console.log( data );

Обычно, но неверно полагать, что данные доступны сразу после вызова $.ajax , но на самом деле происходит следующее:

1
2
3
4
5
6
7
xmlhttp.open( «GET», «some/ur/1», true );
xmlhttp.onreadystatechange = function( data ) {
    if ( xmlhttp.readyState === 4 ) {
        console.log( data );
    }
};
xmlhttp.send( null );

Базовый объект XmlHttpRequest (XHR) отправляет запрос, и функция обратного вызова настроена для обработки события readystatechange . Затем выполняется метод send XHR. Когда XHR выполняет свою работу, внутреннее событие readystatechange срабатывает каждый раз, когда readyState свойство readyState , и только когда XHR завершает получение ответа от удаленного хоста, выполняется функция обратного вызова.


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

Многие из функций в node.js являются асинхронными. Итак, код, подобный следующему, довольно распространен

01
02
03
04
05
06
07
08
09
10
11
var fs = require( «fs» );
 
fs.exists( «index.js», function() {
    fs.readFile( «index.js», «utf8», function( err, contents ) {
        contents = someFunction( contents );
        fs.writeFile( «index.js», «utf8», function() {
            console.log( «whew! Done finally…» );
        });
    });
});
console.log( «executing…» );

Также часто можно увидеть код на стороне клиента, например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GMaps.geocode({
    address: fromAddress,
    callback: function( results, status ) {
        if ( status == «OK» ) {
            fromLatLng = results[0].geometry.location;
            GMaps.geocode({
                address: toAddress,
                callback: function( results, status ) {
                    if ( status == «OK» ) {
                        toLatLng = results[0].geometry.location;
                        map.getRoutes({
                            origin: [ fromLatLng.lat(), fromLatLng.lng() ],
                            destination: [ toLatLng.lat(), toLatLng.lng() ],
                            travelMode: «driving»,
                            unitSystem: «imperial»,
                            callback: function( e ){
                                console.log( «ANNNND FINALLY here’s the directions…» );
                                // do something with e
                            }
                        });
                    }
                }
            });
        }
    }
});

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

Проблема не в самом языке; это связано с тем, как программисты используют язык — Async Javascript .

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var fromLatLng, toLatLng;
 
var routeDone = function( e ){
    console.log( «ANNNND FINALLY here’s the directions…» );
    // do something with e
};
 
var toAddressDone = function( results, status ) {
    if ( status == «OK» ) {
        toLatLng = results[0].geometry.location;
        map.getRoutes({
            origin: [ fromLatLng.lat(), fromLatLng.lng() ],
            destination: [ toLatLng.lat(), toLatLng.lng() ],
            travelMode: «driving»,
            unitSystem: «imperial»,
            callback: routeDone
        });
    }
};
 
var fromAddressDone = function( results, status ) {
    if ( status == «OK» ) {
        fromLatLng = results[0].geometry.location;
        GMaps.geocode({
            address: toAddress,
            callback: toAddressDone
        });
    }
};
 
GMaps.geocode({
    address: fromAddress,
    callback: fromAddressDone
});

Кроме того, библиотека async.js может помочь в обработке нескольких запросов / ответов Ajax. Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
async.parallel([
    function( done ) {
        GMaps.geocode({
            address: toAddress,
            callback: function( result ) {
                done( null, result );
            }
        });
    },
    function( done ) {
        GMaps.geocode({
            address: fromAddress,
            callback: function( result ) {
                done( null, result );
            }
        });
    }
], function( errors, results ) {
    getRoute( results[0], results[1] );
});

Этот код выполняет две асинхронные функции, и каждая функция принимает обратный вызов «done», который выполняется после завершения выполнения асинхронной функции. По завершении обоих обратных вызовов «done» обратный вызов parallel функции выполняется и обрабатывает любые ошибки или результаты двух асинхронных функций.

От CommonJS / A :

Обещание представляет конечное значение, возвращаемое после однократного завершения операции.

Есть много библиотек, которые включают шаблон обещаний, и у пользователей jQuery уже есть хороший API обещаний. jQuery представил объект Deferred в версии 1.5, и использование конструктора jQuery.Deferred приводит к функции, которая возвращает обещание. Функция, возвращающая обещание, выполняет некоторую асинхронную операцию и разрешает отложенное после завершения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var geocode = function( address ) {
    var dfd = new $.Deferred();
    GMaps.geocode({
        address: address,
        callback: function( response, status ) {
            return dfd.resolve( response );
        }
    });
    return dfd.promise();
};
 
var getRoute = function( fromLatLng, toLatLng ) {
    var dfd = new $.Deferred();
    map.getRoutes({
        origin: [ fromLatLng.lat(), fromLatLng.lng() ],
        destination: [ toLatLng.lat(), toLatLng.lng() ],
        travelMode: «driving»,
        unitSystem: «imperial»,
        callback: function( e ) {
            return dfd.resolve( e );
        }
    });
    return dfd.promise();
};
 
var doSomethingCoolWithDirections = function( route ) {
    // do something with route
};
 
$.when( geocode( fromAddress ), geocode( toAddress ) ).
    then(function( fromLatLng, toLatLng ) {
        getRoute( fromLatLng, toLatLng ).then( doSomethingCoolWithDirections );
    });

Это позволяет выполнить две асинхронные функции, дождаться их результатов, а затем выполнить другую функцию с результатами первых двух вызовов.

Обещание представляет конечное значение, возвращаемое после однократного завершения операции.

В этом коде метод geocode выполняется дважды и возвращает обещание. Затем выполняются асинхронные функции и они вызывают в своих обратных вызовах. Затем, после того как оба вызвали resolve , выполняется функция then , которая возвращает результаты первых двух вызовов в geocode . Затем результаты передаются в getRoute , который также возвращает обещание. Наконец, когда обещание от getRoute разрешено, doSomethingCoolWithDirections обратный вызов doSomethingCoolWithDirections .

События являются еще одним решением для связи, когда асинхронные обратные вызовы заканчивают выполняться. Объект может стать источником и публиковать события, которые могут прослушивать другие объекты. Этот тип событий называется паттерном наблюдателя . Библиотека backbone.js имеет этот тип функциональности, встроенный в Backbone.Events .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
var SomeModel = Backbone.Model.extend({
   url: «/someurl»
});
 
var SomeView = Backbone.View.extend({
    initialize: function() {
        this.model.on( «reset», this.render, this );
 
        this.model.fetch();
    },
    render: function( data ) {
        // do something with data
    }
});
 
var view = new SomeView({
    model: new SomeModel()
});

Существуют другие примеры миксов и библиотеки для генерации событий, такие как jQuery Event Emitter , EventEmitter , monologue.js и node.js со встроенным модулем EventEmitter .

Цикл событий — это очередь функций обратного вызова.

Аналогичный метод публикации сообщений использует шаблон медиатора , используемый в библиотеке postal.js . В схеме посредника посредник для всех объектов слушает и публикует события. В этом подходе один объект не имеет прямой ссылки на другой, тем самым отделяя объекты друг от друга.

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

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

1
2
3
4
5
var doSomethingCoolWithDirections = function( route ) {
    postal.channel( «ui» ).publish( «directions.done», {
        route: route
    });
};

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

01
02
03
04
05
06
07
08
09
10
var UI = function() {
    this.channel = postal.channel( «ui» );
    this.channel.subscribe( «directions.done», this.updateDirections ).withContext( this );
};
 
UI.prototype.updateDirections = function( data ) {
    // The route is available on data.route, now just update the UI
};
 
app.ui = new UI();

Некоторыми другими библиотеками сообщений на основе шаблонов-посредников являются ampify , PubSubJS и radio.js .


JavaScript делает написание асинхронного кода очень простым. Использование обещаний, событий или именованных функций устраняет неприятный «ад обратного вызова». Для получения дополнительной информации об асинхронном JavaScript, ознакомьтесь с разделом «Асинхронный JavaScript: создайте больше адаптивных приложений с меньшим количеством кода» . Многие примеры из этого поста находятся в репозитории Github NetTutsAsyncJS . Клонировать прочь!