Сильной стороной 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 был выбран для языка узла из-за того, как легко писать такой код.
Ajax
Асинхронный 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 . Клонировать прочь!