Асинхронный код счетчика. Асинхронный PHP: Зачем? Перевод

Объявлений переменных и функций, но чтобы превратить эту особенность JS в проблему, надо очень постараться. У синхронного кода на JavaScript есть лишь один серьёзный недостаток: на нём одном далеко не уехать.

Практически каждая полезная JS-программа написана с привлечением асинхронных методов разработки. Здесь в дело вступают функции обратного вызова, в просторечии - «коллбэки». Здесь в ходу «обещания», или Promise-объекты, называемые обычно промисами. Тут можно столкнуться с генераторами и с конструкциями async/await. Асинхронный код, в сравнении с синхронным, обычно сложнее писать, читать и поддерживать. Иногда он превращается в совершенно жуткие структуры вроде ада коллбэков. Однако, без него не обойтись.

Сегодня предлагаем поговорить об особенностях коллбэков, промисов, генераторов и конструкций async/await, и подумать о том, как писать простой, понятный и эффективный асинхронный код.

О синхронном и асинхронном коде Начнём с рассмотрения фрагментов синхронного и асинхронного JS-кода. Вот, например, обычный синхронный код:

Console.log("1") console.log("2") console.log("3")
Он, без особых сложностей, выводит в консоль числа от 1 до 3.

Теперь - код асинхронный:

Console.log("1") setTimeout(function afterTwoSeconds() { console.log("2") }, 2000) console.log("3")
Тут уже будет выведена последовательность 1, 3, 2. Число 2 выводится из коллбэка, который обрабатывает событие срабатывания таймера, заданного при вызове функции setTimeout . Коллбэк будет вызвана, в данном примере, через 2 секунды. Приложение при этом не остановится, ожидая, пока истекут эти две секунды. Вместо этого его исполнение продолжится, а когда сработает таймер, будет вызвана функция afterTwoSeconds .

Возможно, если вы только начинаете путь JS-разработчика, вы зададитесь вопросами: «Зачем это всё? Может быть, можно переделать асинхронный код в синхронный?». Поищем ответы на эти вопросы.

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

В плане интерфейса ограничимся чем-нибудь простым .


Простой интерфейс поиска пользователей GitHub и соответствующих им репозиториев

В примерах выполнение запросов будет выполнено средствами XMLHttpRequest (XHR), но вы вполне можете использовать тут jQuery ($.ajax), или более современный стандартный подход, основанный на использовании функции fetch . И то и другое сводится к использованию промисов. Код, в зависимости от похода, будет меняться, но вот, для начала, такой пример:

// аргумент url может быть чем-то вроде "https://api.github.com/users/daspinola/repos" function request(url) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { // Код обработки успешного завершения запроса } else { // Обрабатываем ответ с сообщением об ошибке } } } xhr.ontimeout = function () { // Ожидание ответа заняло слишком много времени, тут будет код, который обрабатывает подобную ситуацию } xhr.open("get", url, true) xhr.send(); }
Обратите внимание на то, что в этих примерах важно не то, что в итоге придёт с сервера, и как это будет обработано, а сама организация кода при использовании разных подходов, которые вы сможете использовать в своих асинхронных разработках.

Функции обратного вызова С функциями в JS можно делать очень много всего, в том числе - передавать в качестве аргументов другим функциям. Обычно так делают для того, чтобы вызвать переданную функцию после завершения какого-то процесса, который может занять некоторое время. Речь идёт о функциях обратного вызова. Вот простой пример:

// Вызовем функцию "doThis" с другой функцией в качестве параметра, в данном случае - это функция "andThenThis". Функция "doThis" исполнит код, находящийся в ней, после чего, в нужный момент, вызовет функцию "andThenThis". doThis(andThenThis) // Внутри "doThis" обращение к переданной ей функции осуществляется через параметр "callback" , фактически, это просто переменная, которая хранит ссылку на функцию function andThenThis() { console.log("and then this") } // Назвать параметр, в котором окажется функция обратного вызова, можно как угодно, "callback" - это просто распространённый вариант function doThis(callback) { console.log("this first") // Для того, чтобы функция, ссылка на которую хранится в переменной, была вызвана, нужно поместить после имени переменной скобки, "()", иначе ничего не получится callback() }
Используя этот подход для решения нашей задачи, мы можем написать такую функцию request:

Function request(url, callback) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(null, xhr.response) } else { callback(xhr.status, null) } } } xhr.ontimeout = function () { console.log("Timeout") } xhr.open("get", url, true) xhr.send(); }
Теперь функция для выполнения запроса принимает параметр callback , поэтому, после выполнения запроса и получения ответа сервера, коллбэк будет вызван и в случае ошибки, и в случае успешного завершения операции.

Const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` request(userGet, function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) { request(user.repos_url, function handleReposList(err, repos) { if (err) throw err //Здесь обработаем список репозиториев }) }) })
Разберём то, что здесь происходит:

  • Выполняется запрос для получения репозиториев пользователя (в данном случае я загружаю собственные репозитории);
  • После завершения запроса вызывается коллбэк handleUsersList ;
  • Если не было ошибок, разбираем ответ сервера c помощью J SON.parse , преобразовываем его, для удобства, в объект;
  • После этого перебираем список пользователей, так как в нём может быть больше одного элемента, и для каждого из них запрашиваем список репозиториев, используя URL, возвращённый для каждого пользователя после выполнения первого запроса. Подразумевается, что repos_url - это URL для наших следующих запросов, и получили мы его из первого запроса.
  • Когда запрос, направленный на загрузку данных о репозиториях, завершён, вызывается коллбэк, теперь это handleReposList . Здесь, так же как и при загрузке списка пользователей, можно обработать ошибки или полезные данные, в которых содержится список репозиториев пользователя.
Обратите внимание на то, что использование в качестве первого параметра объекта ошибки - это широко распространённая практика, в частности, для разработки с использованием Node.js.

Если придать нашему коду более завершённый вид, снабдить его средствами обработки ошибок и отделить определение функций обратного вызова от кода выполнения запроса, что улучшит читабельность программы, получится следующее:

Try { request(userGet, handleUsersList) } catch (e) { console.error("Request boom! ", e) } function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) { request(user.repos_url, handleReposList) }) } function handleReposList(err, repos) { if (err) throw err // Здесь обрабатываем список репозиториев console.log("My very few repos", repos) }
Этот подход работает, но используя его, мы рискуем столкнуться с проблемами вроде состояния гонки запросов и сложностей с обработкой ошибок. Однако, основная неприятность, связанная с коллбэками, которых, считая то, что происходит в цикле forEach , здесь три, заключается в том, что такой код тяжело читать и поддерживать. Подобная проблема существует, пожалуй, со дня появления функций обратного вызова, она широко известна как ад коллбэков.


Ад коллбэков во всей красе. Изображение взято отсюда .

В данном случае под «состоянием гонки» мы понимаем ситуацию, когда мы не контролируем порядок получения данных о репозиториях пользователей. Мы запрашиваем данные по всем пользователям, и вполне может оказаться так, что ответы на эти запросы окажутся перемешанными. Скажем, ответ по десятому пользователю придёт первым, а по второму - последним. Ниже мы поговорим о возможном решении этой проблемы.

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

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

Const myPromise = new Promise(function(resolve, reject) { // Здесь будет код if (codeIsFine) { resolve("fine") } else { reject("error") } }) myPromise .then(function whenOk(response) { console.log(response) return response }) .catch(function notOk(err) { console.error(err) })
Разберём этот пример:

  • Промис инициализируется с помощью функции, в которой есть вызовы методов resolve и reject ;
  • Асинхронный код помещают внутри функции, созданной с помощью конструктора Promise . Если код будет выполнен успешно, вызывают метод resolve , если нет - reject ;
  • Если функция вызовет resolve , будет исполнен метод.then для объекта Promise , аналогично, если будет вызван reject , будет исполнен метод.catch .
Вот что стоит помнить, работая с промисами:
  • Методы resolve и reject принимают только один параметр, в результате, например, при выполнении команды вида resolve("yey", "works") , коллбэку.then будет передано лишь "yey" ;
  • Если объединить в цепочку несколько вызовов.then , в конце соответствующих коллбэков следует всегда использовать return , иначе все они будут выполнены одновременно, а это, очевидно, не то, чего вы хотите достичь;
  • При выполнении команды reject , если следующим в цепочке идёт.then , он будет выполнен (вы можете считать.then выражением, которое выполняется в любом случае);
  • Если в цепочке из вызовов.then в каком-то из них возникнет ошибка, следующие за ним будут пропущены до тех пор, пока не будет найдено выражение.catch ;
  • У промисов есть три состояния: «pending» - состояние ожидания вызова resolve или reject , а также состояния «resolved» и «rejected», которые соответствуют успешному, с вызовом resolve , и неуспешному, с вызовом reject , завершению работы промиса. Когда промис оказывается в состоянии «resolved» или «rejected», оно уже не может быть изменено.
Обратите внимание на то, что промисы можно создавать без использования отдельно определённых функций, описывая функции в момент создания промисов. То, что показано в нашем примере - лишь распространённый способ инициализации промисов.

Для того, чтобы не погрязнуть в теории, вернёмся к нашему примеру. Перепишем его с использованием промисов.

Function request(url) { return new Promise(function (resolve, reject) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.response) } else { reject(xhr.status) } } } xhr.ontimeout = function () { reject("timeout") } xhr.open("get", url, true) xhr.send(); }) }
При таком подходе, когда вы вызываете request , возвращено будет примерно следующее.

Это - промис в состоянии ожидания. Он может быть либо успешно разрешён, либо отклонён

Теперь, воспользовавшись новой функцией request , перепишем остальной код.

Const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const myPromise = request(userGet) console.log("will be pending when logged", myPromise) myPromise .then(function handleUsersList(users) { console.log("when resolve is found it comes here with the response, in this case users ", users) const list = JSON.parse(users).items return Promise.all(list.map(function(user) { return request(user.repos_url) })) }) .then(function handleReposList(repos) { console.log("All users repos in an array", repos) }) .catch(function handleErrors(error) { console.log("when a reject is executed it will come here ignoring the then statement ", error) })
Здесь мы оказываемся в первом выражении.then при успешном разрешении промиса. У нас имеется список пользователей. Во второе выражение.then мы передаём массив с репозиториями. Если что-то пошло не так, мы окажемся в выражении.catch .

Благодаря такому подходу мы разобрались с состоянием гонки и с некоторыми возникающими при этом проблемами. Ада коллбэков тут не наблюдается, но код пока ещё читать не так-то легко. На самом деле, наш пример поддаётся дальнейшему улучшению за счёт выделения из него объявлений функций обратного вызова:

Const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const userRequest = request(userGet) // Если просто прочитать эту часть программы вслух, можно сразу понять что именно делает код userRequest .then(handleUsersList) .then(repoRequest) .then(handleReposList) .catch(handleErrors) function handleUsersList(users) { return JSON.parse(users).items } function repoRequest(users) { return Promise.all(users.map(function(user) { return request(user.repos_url) })) } function handleReposList(repos) { console.log("All users repos in an array", repos) } function handleErrors(error) { console.error("Something went wrong ", error) }
При таком подходе один взгляд на имена коллбэков в выражениях.then раскрывает смысл вызова userRequest . С кодом легко работать, его легко читать.

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

Генераторы Ещё один подход к решению нашей задачи, который, однако, нечасто встретишь - это генераторы. Тема это немного более сложная, чем остальные, поэтому, если вы чувствуете, что вам это изучать пока рано, можете сразу переходить к следующему разделу этого материала.

Для того, чтобы определить функцию-генератор, можно воспользоваться знаком звёздочки, «*», после ключевого слова function . С помощью генераторов асинхронный код можно сделать очень похожим на синхронный. Например, выглядеть это может так:

Function* foo() { yield 1 const args = yield 2 console.log(args) } var fooIterator = foo() console.log(fooIterator.next().value) // выведет 1 console.log(fooIterator.next().value) // выведет 2 fooIterator.next("aParam") // приведёт к вызову console.log внутри генератора и к выводу "aParam"
Дело тут в том, что генераторы, вместо return , используют выражение yield , которое останавливает выполнение функции до следующего вызова.next итератора. Это похоже на выражение.then в промисах, которое выполняется при разрешении промиса.

Посмотрим теперь, как это всё применить к нашей задаче. Итак, вот функция request:

Function request(url) { return function(callback) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { callback(null, xhr.response) } else { callback(xhr.status, null) } } } xhr.ontimeout = function () { console.log("timeout") } xhr.open("get", url, true) xhr.send() } }
Тут, как обычно, мы используем аргумент url , но вместо того, чтобы сразу выполнить запрос, мы хотим его выполнить только тогда, когда у нас будет функция обратного вызова для обработки ответа.

Генератор будет выглядеть так:

Function* list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = yield request(userGet) yield for (let i = 0; i console.log("after 2 seconds", result)) async function sumTwentyAfterTwoSeconds(value) { const remainder = afterTwoSeconds(20) return value + await remainder } function afterTwoSeconds(value) { return new Promise(resolve => { setTimeout(() => { resolve(value) }, 2000); }); }
Здесь происходит следующее:

  • Имеется асинхронная функция sumTwentyAfterTwoSeconds ;
  • Мы предлагаем коду подождать разрешения промиса afterTwoSeconds , который может завершиться вызовом resolve или reject ;
  • Выполнение кода заканчивается в.then , где завершается операция, отмеченная ключевым словом await , в данном случае - это всего одна операция.
Подготовим функцию request к использовании в конструкции async/await:

Function request(url) { return new Promise(function(resolve, reject) { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(e) { if (xhr.readyState === 4) { if (xhr.status === 200) { resolve(xhr.response) } else { reject(xhr.status) } } } xhr.ontimeout = function () { reject("timeout") } xhr.open("get", url, true) xhr.send() }) }
Теперь создаём функцию с ключевым словом async , в которой используем ключевое слово await:

Async function list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = await request(userGet) const usersList = JSON.parse(users).items usersList.forEach(async function (user) { const repos = await request(user.repos_url) handleRepoList(user, repos) }) } function handleRepoList(user, repos) { const userRepos = JSON.parse(repos) // Обрабатываем тут репозитории для каждого пользователя console.log(user, userRepos) }
Итак, у нас имеется асинхронная функция list , которая обработает запрос. Ещё конструкция async/await нам понадобится в цикле forEach , чтобы сформировать список репозиториев. Вызвать всё это очень просто:

List() .catch(e => console.error(e))
Этот подход и использование промисов - мои любимые методы асинхронного программирования. Код, написанный с их использованием, удобно и читать и править. Подробности об async/await можно почитать .

Минус async/await , как и минус генераторов, заключается в том, что эту конструкцию не поддерживают старые браузеры, а для её использования в серверной разработке нужно пользоваться Node 8. В подобной ситуации, опять же, поможет транспилятор, например - babel .

Итоги можно посмотреть код проекта, который решает поставленную в начале материала задачу с использованием async/await . Если вы хотите как следует разобраться с тем, о чём мы говорили - поэкспериментируйте с этим кодом и со всеми рассмотренными технологиями.

Обратите внимание на то, что наши примеры можно улучшить, сделать лаконичнее, если переписать их с использованием альтернативных способов выполнения запросов, вроде $.ajax и fetch . Если у вас есть идеи о том, как улучшить качество кода при использовании вышеописанных методик - буду благодарен, если расскажете об этом мне .

В зависимости от особенностей поставленной перед вами задачи, может оказаться так, что вы будете пользоваться async/await, коллбэками, или некоей смесью из разных технологий. На самом деле, ответ на вопрос о том, какую именно методику асинхронной разработки выбрать, зависит от особенностей проекта. Если некий подход позволяет решить задачу с помощью читабельного кода, который легко поддерживать, который понятен (и будет понятен через некоторое время) вам и другим членам команды, значит этот подход - то, что вам нужно.

Уважаемые читатели! Какими методиками написания асинхронного кода на JavaScript вы пользуетесь?

Теги:

  • JavaScript
  • разработка
  • callback
  • async
  • await
  • promise
  • generator
  • асинхронный код
Добавить метки

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

Отзывчивость

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

Параллельность vs асинхронность

Большинство людей, когда они видят асинхронный код, сразу думают “О, это круто! Я могу запустить мои задачи параллельно!” . Я, может вас разочарую, но на самом деле, это неправда, конкурентность (асинхронность) и параллельность — это не то же самое. Это часто встречающаяся ошибка, поэтому давайте попробуем разобраться почему.

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

Асинхронность:

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

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

Параллельность:

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

Чтобы проиллюстрировать разницу между асинхронным и параллельным выполнением на реальных примерах, мы можем сравнить два популярных веб-сервера: Apache и Nginx. Они прекрасно иллюстрируют эту разницу: Nginx является асинхронным и основан на событиях, в то время как Apache использует параллельные потоки. Apache создает новые потоки для каждого дополнительного подключения, поэтому существует максимально допустимое количество соединений в зависимости от доступной памяти в системе. При достижении этого лимита подключений, Apache отказывается от дополнительных соединений. Ограничивающим фактором в настройке Apache является память (помните, что параллельное выполнение часто зависит от аппаратного обеспечения). Если поток останавливается, клиент ждет ответа, пока поток освободится и вернёт ответ.

Nginx работает иначе, чем Apache и не создаёт новые потоки для каждого входящего запроса. У него есть основной процесс-воркер (или несколько процессов, зачастую на практике рекомендуется иметь по одному воркеру на каждый процессор(CPU)), который является однопоточным. Этот воркер может обрабатывать тысячи одновременных подключений. Он делает это асинхронно с одним потоком, а не с помощью многопоточного параллельного выполнения.

Итак, асинхронность (или конкурентность) — это способ выполнения задач. Она представляет собой композицию из независимо выполняющихся задач. Параллельность — это одновременное выполнение нескольких задач (они могут быть связаны, а могут и нет). В асинхронности, мы имеем дело со многими разными задачами одновременно. Параллельность делает много задач сразу. Звучит одинаково, но в основе разные идеи. Конкурентность — это о структуре, в то время как параллельность — об исполнении.

От переводчика:

Асинхронность : у вас есть бекенд задача, которую вы делаете в данный момент. Тут к вам обратился фронтенд разработчик с просьбой немого поправить API. Вы сохранились у себя в ветке, переключились на другую (в данный момент вы заняты задачей с API), поправили и вернулись к своей задаче. После вас позвали на совещание по какой-то тематике, вы прервались, сходили (в данный момент вы выполняете третью задачу, которая будет продолжена на следующем совещании) и вернулись опять к своей первой задаче. Эти три задачи вы выполняли асинхронно — другими словами, прерываясь и переключаясь между задачами, выделяя на них понемногу времени.

Параллельность : если говорить о параллельности в лице одного человека, то в голову приходят пара очевидных примеров. Например, играть на гитаре и петь. В этот момент одна ваша рука ставит аккорды, другая выполняет перебор (или бой) и плюс вы ещё поёте. В этот момент вы параллельно выполняете три задачи. Или другой пример: вы за обедом слушаете Моцарта. Тут вы параллельно выполняете две задачи — едите и слушаете. Но если вернуться к задачам разработки, то получится более наглядный пример. Представьте, что вы работаете в команде из 4 разработчиков. Ваша команда — это эдакая единая машина с четырехъядерным процессором. Каждый разработчик — одно ядро. Когда начинается спринт, каждый из 4 разработчиков выполняет свои задачи параллельно с остальными тремя разработчиками, а в конце спринта вы собираете это воедино.

Зачем на бекенде?

Теперь вы можете возмутиться на то, что на беке вас не особо волнует отзывчивость. У вас на фронте есть все эти гадкие вещи типа асинхронного JavaScript-а, а всё, что должен делать ваш сервер, — это просто отвечать на запросы. Так что обеспечить отзывчивость пользователю — это задача фронта, а не ваша. Да, это правда, но бекенд не ограничивается только API-ответами. Иногда вам нужно управлять какими-то сложными задачами, например, сервер для загрузки видео. В данном случае, возможно, отклик не является ключевым фактором, но мы упираемся в нехватку ресурсов , потому что приложение должно ждать. Он может ждать операций файловой системы, сетевого подключения, запросов к базе и так далее. Часто эти операции ввода/вывода осуществляются крайне медленно по сравнению с расчетами на CPU, например, когда мы конвертируем видеофайлы. А пока мы медленно сохраняем или читаем файл, наш процессор должен ждать и ничего не делать, вместо того, чтобы делать какую-то полезную работу. Как мы уже говорили, вместо ожидания, мы можем выполнять эти задачи в фоновом режиме. Как? Читайте ниже.

Асинхронный РНР

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

SetTimeout(function() { console.log("After timeout"); }, 1); console.log("Before timeout");

При выполнении этого кода мы видим следующее:

Before timeout After timeout

Функция setTimeout() ставит в очередь код, который будет выполняться после завершения текущего стека вызовов. Это означает, что мы нарушаем синхронный поток кода и откладываем выполнение некоторой части кода. Второй вызов console.log() будет выполнен до первого (внутри функции settimeout()), который был поставлен в очередь.

Но как на счёт PHP? Ну, в PHP из коробки у нас нет хороших и удобных инструментов для написания действительно асинхронного кода. Нет функции эквивалентной settimeout() и мы просто не можем отложить или поставить в очередь какой-то код. Вот почему такие фреймворки и библиотеки, как Amp и ReactPHP , начали появляться. Их основная идея в том, чтобы скрыть от нас низкоуровневые тонкости языка и предоставить инструменты и абстракции высокого уровня, которые могут быть использованы для написания асинхронного кода и управления конкурентностью, как мы могли бы сделать это в JavaScript и NodeJS.

Почему я должен использовать PHP, если у нас есть NodeJs и Go?

Подобный вопрос возникает чаще всего, когда речь идет об асинхронном РНР. Почему-то сообщество часто против использования PHP в качестве инструмента для написания асинхронного кода. Всегда кто-то предлагает просто использовать Go и NodeJs .

Сегодня каждый день появляются новые языки программирования - Go, Rust, CoffeeScript - все, что угодно. Я решил, что я тоже горазд придумать свой язык программирования, что миру не хватает какого-то нового языка…

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

Что в нем не очень обычно, что может даже оттолкнуть, на первый взгляд, - это то, что в Schlecht!Script функции имеют цвет.


Т.е., когда вы объявляете функцию, когда вы ее вызываете, вы явным образом указываете ее цвет.

Функции бывают красные и синие - двух цветов.

Важный момент: внутри синих функций вы можете вызывать только другие синие функции. Вы не можете вызывать красные функции внутри синих функций.


Внутри красных функций вы можете вызывать и красные, и синие функции.


Я решил, что должно быть так. В каждом языке должно быть так.

Тонкий момент: красные функции писать и вызывать больно! Что я имею в виду, когда говорю «больно»? Дело в том, что сейчас я изучаю немецкий язык, и я решил, что красные функции мы должны все называть только на немецком языке, иначе интерпретатор просто не поймет, что вы ему пытаетесь впихнуть, и он просто не будет это выполнять.


Вот так вы должны писать функции на немецком языке:


«!» обязателен - мы же на немецком пишем, в конце концов.

Как писать на таком языке? У нас есть два способа. Мы можем использовать только синие функции, в которые писать не больно, но внутри мы не можем пользоваться красными функциями. Этот подход не будет работать, потому что в порыве вдохновения я написал половину стандартной библиотеки на красных функциях, так что, простите…

Вопрос к вам - стали бы вы использовать такой язык? Продал ли я вам Schlecht!Script?

Ну, у вас, как бы, нет выбора. Простите…

JavaScript - отличный язык, мы все его любим, мы все здесь собрались, потому что мы любим JavaScript. Но проблема в том, что JavaScript наследует некоторые черты Schlecht!Script, и я, конечно, не хочу хвастаться, но, по-моему, они украли пару моих идей.

Что именно они наследуют? в JavaScript есть красные и синие функции. Красные функции в JavaScript - это асинхронные функции, синие - синхронные функции. И все прослеживается, все та же цепочка… Красные функции вызывать больно в Schlecht!Script, а асинхронные функции вызывать больно в JavaScript.

И внутри синих функций мы не можем писать красные функции. Я еще скажу об этом позже.


Почему это больно? Откуда боль при вызове и при написании асинхронных функций?

У нас по-другому работают условные операторы, циклы, return. У нас не работает try/catch, и асинхронные функции ломают абстракцию.

О каждом пункте немного подробнее.


Вот так выглядит синхронный код, где shouldProcess и process - это функции синхронные, и работают условные операторы, работает for, в общем, все хорошо.

То же самое, но асинхронное, будет выглядеть вот так вот:

Там появилась рекурсия, мы передаем состояние в параметры, в функцию. В общем, смотреть прямо-таки неприятно. У нас не работает try/catch и, я думаю, мы все знаем, что если мы обернем синхронный блок кода в try/catch, исключения мы не поймаем. Нам нужно будет передать callback, перенавесить обработчик событий, в общем, у нас нет try/catch…

И асинхронные функции ломают абстракцию. Что я имею в виду? Представьте, что вы написали кэш. Вы сделали кэш пользователей в памяти. И у вас есть функция, которая читает из этого кэша, которая, естественно, синхронная, потому что все в памяти. Завтра к вам приходят тысячи, миллионы, миллиарды пользователей, и вам нужно положить этот кэш в Redis. Вы кладете кэш в Redis, функция становится асинхронной, потому что из-за Redis"а мы можем читать только асинхронно. И, соответственно, весь стек, который вызывал вашу синхронную функцию, придется переписать, потому что теперь весь стек становится асинхронным. Если какая-то функция зависела от функции чтения из кэша, она так же будет теперь асинхронной.

В общем и целом, говоря об асинхронности в JavaScript, можно сказать, что все там грустно.

Но что мы все об асинхронности? Давайте немного обо мне поговорим, наконец.


Я пришел, чтобы вас всех спасти. Ну, я попробую это сделать.

Меня зовут Андрей, я работаю в стартапе «Productive Mobile» в Берлине. Я помогаю с организацией MoscowJS и я являюсь соведущим RadioJS. Мне очень интересна тема асинхронности и не только в JavaScript, я считаю, что, в принципе, это определяющий момент языка. То, как язык работает с асинхронностью, определяет его успех и то, насколько людям приятно и удобно с ним работать.

Говоря об асинхронности конкретно в JavaScript, мне кажется, у нас есть два сценария, с которыми мы постоянно взаимодействуем. Это обработка множества событий и обработка единичных асинхронных операций.

Множество событий - это, например, событие DOM или подключение к серверу - что-то, что излучает множество нескольких типов событий.

Единичная операция - это, к примеру, чтение из базы. Единичная асинхронная операция возвращает нам либо один результат, либо возвращает ошибку. Больше никаких вариантов нет.

И, говоря об этих двух сценариях, интересно порассуждать: вот, типа, асинхронность - плохо, в общем, все грустно… А что мы на самом деле хотим? Как бы выглядел идеальный асинхронный код?


А хотим мы, мне кажется, контроля потока управления. Мы хотим, чтобы наши условные операторы, циклы работали в синхронном коде так же, как в асинхронном.

Мы хотим обработки исключений. Зачем нам try/catch, если мы не можем его использовать в асинхронных операциях? Это просто странно.

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

Вот, чего мы хотим.

А что у нас есть сегодня, и какие инструменты у нас появятся в будущем?


Если мы говорим об ECMAScript 6 (это, в принципе, то, о чем я буду говорить сегодня), для работы с множеством событий у нас есть EventEmitter и Stream, а для работы с единичными асинхронными операциями - Continuation Passing Style (они же callback"и), Promises и Coroutines.


В ECMAScript 7 у нас появятся Async Generators для работы с множеством событий и Async/Await - для работы с единичными асинхронными операциями.

Об этом и поговорим.

Начнем с того, что у нас есть в ECMAScript 6 для работы с множеством асинхронных событий. Напомню, например, это обработка событий мыши или нажатий на клавиши. У нас есть паттерн EventEmitter, который реализован в браузере в Node.js. Он встречается практически в любой API, где мы работаем с множеством событий. EventEmitter говорит нам, что мы можем создать объект, который излучает события, и навешивать обработчики на каждый тип события.


Интерфейс очень простой. Мы можем добавлять EventListener, убирать EventListener по названию event"а, передавая туда сallback.


К примеру, в XMLHttpRequest, когда я говорю о множестве событий, я имею в виду, что у нас может быть множество событий progress. Т.е. по мере того, как мы загружаем какие-то данные с помощью AJAX-запроса, нам выстреливают события progress, и по одному разу выстреливают события load, abort и error:


Error - это особенное событие, универсальное событие в EventEmitter"ах и в Stream"ах для того чтобы уведомить пользователя об ошибке.

Есть множество реализаций:


Здесь перечислены только несколько, и в конце доклада будет ссылка, где все эти реализации есть.

Важно сказать, что в Node.js EventEmitter встроен по умолчанию.

Итак, это то, что у нас есть практически по стандарту в API и браузерах в Node.js.

Что у нас еще есть для работы с множеством событий? Stream.

Stream - это поток данных. Что такое данные? Это могут быть бинарные данные, например, данные из файла, текстовые данные, либо объекты или события. Самые популярные примеры:


Есть несколько типов потоков:


Здесь мы рассматриваем цепочку преобразований из Stylus файлов в css файлы, добавляя автопрефиксер, потому что все мы любим Андрея Ситника и его автопрефиксер.

Вы видите, что у нас есть несколько типов потоков - поток-источник это gulp.src, который читает файлы и излучает объекты файлы, которые потом идут в потоки преобразования. Первый поток преобразования делает из stylus файла css, второй поток преобразования добавляет префиксы. И последний тип потоков - это поток-потребитель, который принимает эти объекты, пишет что-то куда-то на диск, и ничего не излучает.


Т.е. у нас есть 3 типа потоков - источник данных, преобразование и потребитель. И эти паттерны прослеживаются везде, не только в gulp, но и при наблюдении за DOM событиями. У нас есть потоки, которые излучают DOM-события, которые их преобразуют и что-то, что потребляя эти DOM-события, возвращает конкретный результат.

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

Есть несколько реализаций потоков, или они же Observables:


В Node.js потоки встроенные - это Node Streams.

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


Когда мы говорим о тех критериях, по которым мы сравниваем асинхронные API, у нас не работают, по большому счету, операторы return, операторы-циклы, у нас не работают try/catch, естественно, и до единого интерфейса с синхронными операциями нам еще далеко.

В общем, для работы с множеством событий в ECMAScript 6 все не очень хорошо.

Когда мы говорим об единичных асинхронных операциях, у нас есть 3 подхода в ECMAScript 6:


Continuation Passing Style, они же - callback"и.


Я думаю, вы все к этому уже привыкли. Это когда мы делаем асинхронный запрос, передаем туда callback, и callback будет вызван либо с ошибкой, либо с результатом. Это распространенный подход, он есть и в браузере в Node.


Проблемы с этим подходом, я думаю, вы тоже все понимаете.


Вот так бы мы получали ленту твитов пользователей асинхронно, если бы все функции были синхронными.


Тот же самый код, но синхронно, выглядит следующим образом:


Можно увеличить шрифт, чтобы было виднее. И еще немного увеличить… И мы прекрасно понимаем, что это Schlecht!Script.


Я говорил, что они украли мою идею.

Continuation Passing Style - это стандартный API в браузере в Node.js, мы работаем с ними постоянно, но это неудобно. Поэтому у нас есть Promises. Это объект, который представляет собой асинхронную операцию.


Promise - это как бы обещание что-то сделать в будущем, асинхронно.


Мы можем навешивать callback"и на асинхронную операцию с помощью метода then, и это, в принципе, основной метод в API. И мы можем, очень важно, чейнить Promises, мы можем вызывать then последовательно, и каждая функция, которая передается в then, также может возвращать Promises. Вот так выглядит запрос ленты пользователя из твиттера на Promises.

Если мы сравниваем этот подход с Continuation Passing Style, Promises, естественно, удобнее пользоваться - они дают нам возможность писать гораздо меньше бойлерплейта.

Continuation Passing Style, по-прежнему, используется во всех API по умолчанию, в Node.js, в io.js., и они даже не планируют переходы на Promises по нескольким причинам. Сначала многие говорили, что причины - это производительность. И это действительно так, исследования 2013 года показывают, что Promises сильно позади callback"ов. Но с появлением таких библиотек как bluebird, мы уже можем уверенно сказать, что это не так, потому что Promises в bluebird приближаются в производительности к callback"ам. Важный момент: почему Promises не рекомендуют использовать в API до сих пор? Потому что, когда вы выдаете Promises из вашей API, вы навязываете имплементацию.

Все Promises библиотеки должны подчиняться стандарту, но выдавая Promises, вы так же выдаете и имплементацию, т.е. если вы написали свой код, используя медленные Promises, и выдаете медленный Promises из API, это будет не очень приятно пользователям. Поэтому для внешних API, конечно же, по-прежнему рекомендуют использовать callback"и.


Реализаций Promises - масса, и если вы не написали свою реализацию, вы ненастоящий JavaScript-программист. Я не написал свою реализацию Promises, поэтому мне пришлось придумать свой язык.

Итак, Promises, в общем, чуть меньше бойлерплейта, но, тем не менее, все еще не так хорошо.

Что по поводу Coroutines? Здесь уже начинаются интересные штуки. Представьте…

Это интересно. Мы были на JSConf в Будапеште, и там был сумасшедший человек, который на JavaScript программировал квадракоптер и что-то еще, и половину из того, что он пытался нам показать, у него не получалось. Поэтому он постоянно говорил: «OK, теперь представьте… Этот квадракоптер взлетел и все получилось...».

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


Здесь функция получает имя пользователя, она лезет в базу, получает объект пользователя, возвращает его имя. Естественно, «залезть в базу» - функция getUser асинхронная. Что, если мы могли бы поставить функцию getUserName на паузу в момент вызова getUser? Вот, мы выполняем нашу getUserName функцию, дошли до getUser и остановились. getUser сходил в базу, получил объект, вернул его в функцию, мы продолжаем выполнение. Насколько круто это было бы.

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


Это не блокирующая операция. Мы останавливаем выполнение конкретной функции в конкретном месте.


Как getUserName выглядит с помощью генераторов на JavaScript? Нам нужно добавить «*» в объявление функции, чтобы сказать о том, что функция возвращает генератор. Мы можем использовать ключевое слово «yield» в том месте, где мы хотим поставить функцию на паузу. И важно помнить, что getUser здесь возвращает Promises.

Т.к. генераторы изначально придумали для того чтобы делать ленивые последовательности в JavaScript, по большому счету, использовать их для синхронного кода - это хак. Поэтому нам нужны библиотеки, чтобы как-то это компенсировать.


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

Итого, вот, что у нас получается:


У нас функция, внутри которой мы можем использовать if, for и другие операторы.

Чтобы вернуть значение, мы просто пишем return, так же, как в синхронной функции. Мы можем использовать try/catch внутри, и мы поймаем исключение.


Если Promises с getUser разрешится с ошибкой, это выкинется как исключение.

getUserName функция возвращает Promises, поэтому можем работать с ним так же, как с любым Promises, можем навешивать callback"и с помощью then, чейнить и т.д.

Но, как я уже сказал, использовать генераторы для асинхронного кода - это хак. Поэтому экспоузить в качестве внешней API нежелательно. Но использовать внутри приложения нормально, так что пользуйтесь, если у вас есть возможность транспайлить ваш код.


Есть множество реализаций. Какие-то используют генераторы, которые уже часть стандарта, есть Fibers, которые в Node.js работают и которые не используют генератор, а свои заморочки у них.

В общем, это третий подход для работы с единичными асинхронными операциями, и это пока еще хак, но мы уже можем использовать код, который приближен к синхронному. Мы можем использовать условные операторы, циклы и блоки try/catch.


Т.е. ECMAScript 6 для работы с единичными асинхронными операциями как бы немного приближает нас к желаемому результату, но по-прежнему проблема единого интерфейса не решена, даже в Coroutines, потому что нам нужно писать «*» специальную и использовать ключевой оператор «yield».


Итак, в ECMAScript 6 для работы с множеством событий у нас есть EventEmitter и Stream, для работы с единичными асинхронными операциями - CPS, Promises, Coroutines. И все это, вроде бы, здорово, но чего-то не хватает. Хочется большего, чего-то отважного, смелого, нового, хочется революции.

И ребята, которые пишут ES7, решили дать нам революцию и принесли для нас Async/Await и Async Generators.


Async/Await - это стандарт, который позволяет нам работать с единичными асинхронными операциями, такими, как, например, запросы в БД.

Вот так мы писали getUserName на генераторах:


А вот так тот же код выглядит с помощью Async/Await:


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


И так же у нас работают условные операторы, циклы, и try/catch, то бишь, асинхронные функции легализованы в ES7. Теперь мы явно говорим, что если функция асинхронная, то добавьте ключевое слово «async». И это, в принципе, не так плохо, но опять же единого интерфейса у нас нет.

Что по поводу множества событий? Здесь у нас есть стандарт, который называется Async Generators.

Что такое, вообще, множество? Как мы работаем с множеством в JavaScript?


C множеством мы работаем при помощи циклов, так давайте работать с множеством событий при помощи циклов.


Внутри асинхронной функции мы можем использовать ключевую конструкцию «for… on», которая нам позволяет итерировать по асинхронным коллекциям. Как бы.

В данном примере observe возвращает нам что-то, по чему мы можем итерировать, т.е. каждый раз, когда пользователь будет двигать мышкой, у нас будет появляться событие «mousemove». Мы попадаем в этот цикл, и обрабатываем как-то это событие. В данном случае рисуем черточки на экране.


Т.к. функция асинхронная, важно понимать, что она возвращает Promise. Но что, если мы хотим вернуть множество значений, если мы хотим, например, обрабатывать как-то сообщения из веб-сокета, фильтровать их? Т.е. у нас поступает множество и на выходе у нас множество. Здесь нам помогают асинхронные генераторы. Мы пишем «async function *» и говорим о том, что функция асинхронна, и мы возвращаем множество какое-то.


В данном случае мы смотрим на событие Message на веб-сокете и каждый раз, когда оно происходит, мы делаем какую-то проверку и, если проверка проходит, мы в возвращаемую коллекцию. Как бы добавляем этот Message.


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

Вопрос: что возвращает filterWSMessages?


Это точно не Promise, потому что это какая-то коллекция, что-то в этом роде… Но это и не массив.


Даже больше. Что возвращают эти Observe, которые генерят события?

А возвращают они т.н. объекты Observables. Это новое слово, но по большому счету, Observables - это потоки, это Streams. Т.о., круг замыкается.

Итого, у нас есть для работы с асинхронными единичными операциями Async/Await, для работы с множеством - Async Generators.

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

Чтобы получить ленту твитов, в CPS мы писали бы вот такой код:


Много бойлерплейта, обработка ошибок, ручная практически и, в общем, не очень приятная.

С помощью Promise код выглядит таким вот образом:


Бойлерплейт поменьше, мы можем обрабатывать исключения в одном месте, что уже хорошо, но, тем не менее, есть эти then..., не работают ни try/ catch, ни условные операторы.

С помощью Async/Await получаем такую конструкцию:


И примерно то же нам дают Coroutines.

Здесь все шикарно, за исключением того, что нам нужно объявить эту функцию как «async».

Что касается множества событий, если мы говорим о DOM-событиях, вот так мы обрабатывали бы mousemove и рисовали бы по экрану с помощью EventEmitter"а:


Тот же самый код, но с помощью Stream"ов и библиотеки Kefir выглядит таким образом:


Мы создаем поток из событий mousemove на window, мы их каким-то образом фильтруем, и на каждое значение мы вызываем callback функцию. И, когда мы вызовем end у этого stream"а, мы автоматически отпишемся от событий в DOM, что немаловажно

Async Generators выглядит таким образом:


Это просто цикл, мы итерируем по коллекции асинхронных событий и производим какие-то операции над ними.

По-моему, это огромный путь.

Хотелось бы в заключение сказать пару слов о том, как, собственно, перестать отлаживать асинхронный код и начать жить.

  • Определите вашу задачу, т.е. если вы работаете с множеством событий, имеет смысл посмотреть на Streams или, возможно даже, на Async Generators, если у вас есть транспайлер.
  • Если вы работаете с базой, например, отправляете туда запросы или отправляете AJAX-запросы, которые могут либо зафейлиться, либо выполниться, используйте Promises.
  • Обдумайте ваши ограничения. Если вы можете использовать транспайлер, имеет смысл посмотреть на Async/Await и Async Generators. Если вы пишете API, возможно, не имеет смысла экспоузить Promise в качестве внешней API и делать все на callback’ах.
  • Используйте лучшие практики, помните про события error на потоках и на EventEmitter"ах.
  • Помните про специальные методы вроде Promise.all и т.д.

Я знаю, всех вас интересует судьба Schlecht!Script, когда он будет выложен на GitHub и т.д., но дело в том, что из-за постоянной критики, обвинений в плагиате - говорят, что язык такой уже есть, ничего нового я не изобрел, я решил закрыть проект и посвятить себя, может быть, чему-то полезному, важному, интересному, возможно, я даже напишу свою библиотеку Promises.

Коды (асинхронный и синхронный): формирование и установка

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

Ранее в Google Analytics предлагался стандартный код, который влиял на скорость загрузки страницы и на сбор данных.

На текущий момент Google Analytics предлагает асинхронную версию кода, которая обладает рядом преимуществ относительно стандартного кода.

Асинхронный код – это улучшенный фрагмент кода JavaScript, который загружает код отслеживания ga.js в фоновом режиме, без приостановки загрузки других скриптов и контента на страницах вашего веб-сайта.

Преимущества использования асинхронного кода:

  • Более точный сбор данных о коротких посещениях
  • Если клики произошли до загрузки кода, данные будут сохранены
  • Если у вас установлена стандартная версия кода, рекомендуется обновить код на асинхронный. Для этого необходимо удалить старый фрагмент кода, и на его место встроить асинхронный код. После установки асинхронного кода данные начнут появляться в аккаунте в течение 24 часов.

    • не использовать 2 кода одновременно, это может привести к неточности данных;
    • асинхронный код располагается внизу раздела , в отличие от стандартного, который располагался непосредственно перед закрывающим тегом .

    Формирование и установка кода

    1. Код создается на уровне профиля. Необходимо перейти на вкладку «Администратор».

    3. Полученный код необходимо скопировать и встроить на страницу перед закрывающим тегом .

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

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

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

    Базовое описание асинхронного кода

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

    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 мс - “c”, “d”, и “e”. Я использую термин “примерно” потому, что в действительности setTimeout работает непредсказуемо. Даже в спецификации HTML5 указано: "Таким образом, API не гарантирует, что таймер выполнится точно по заданному расписанию. Вероятны задержки из-за нагрузки процессора, других задач и прочих факторов."

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

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

    AJAX

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

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

    Обычно, но неправильно, предполагается, что данные станут доступны сразу после вызова $.ajax . Но действительность выглядит иначе:

    Xmlhttp.open("GET", "some/ur/1", true); xmlhttp.onreadystatechange = function(data) { if (xmlhttp.readyState === 4) { console.log(data); } }; xmlhttp.send(null);

    Схожий метод публикации событий используется в шаблоне с посредником (mediator pattern), который применяется в библиотеке postal.js . В шаблоне с посредником имеется доступный для всех объектов посредник, который ловит и публикует события. При таком подходе один объект не имеет прямых ссылок на другой объект, а следовательно, все объекты отвязаны друг от друга.

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

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

    Var doSomethingCoolWithDirections = function(route) { postal.channel("ui").publish("directions.done", { route: route }); };

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

    Var UI = function() { this.channel = postal.channel("ui"); this.channel.subscribe("directions.done", this.updateDirections).withContext(this); }; UI.prototype.updateDirections = function(data) { // Маршрут доступен в data.route, теперь нужно просто обновить интерфейс }; app.ui = new UI();

    Другие реализации шаблона с посредником используются в библиотеках amplify , PubSubJS и radio.js .

    Заключение

    JavaScript делает простым процесс создания асинхронных приложений. Использование обещаний, событий или именованных функций позволяет избежать "ада возвратных функций".



    
    Top