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

Одна из сильных сторон 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 делает простым процесс создания асинхронных приложений. Использование обещаний, событий или именованных функций позволяет избежать "ада возвратных функций".

Объявлений переменных и функций, но чтобы превратить эту особенность 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 вы пользуетесь?

Теги:

Добавить метки

Итак, клонируете репозиторий и переходите на ветку Part_1:

Git clone https://github.com/Peleke/promises/ git checkout Part_1-Basics

Вы на пути к истине. Наш маршрут включает в себя следующие вопросы:

  • Проблема функций обратного вызова
  • Промисы: определения и замечания из спецификации
  • Промисы и не-инверсия управления
  • Управление потоком с промисами
  • Осознаем смысл then , reject и resolve
Асинхронность

Если вы достаточно времени работали с JavaScript, то вы уже слышали, что он фундаментально неблокирующий или асинхронный . Но что это означает?

Синхронный и асинхронный код

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

// readfile_sync.js "use strict"; // Этот пример из Node, поэтому не запускайте его в браузере. const filename = "text.txt", fs = require("fs"); console.log("Reading file . . . "); // readFileSync БЛОКИРУЕТ выполнение до возврата значения. // Программа будет ждать и ничего не выполнять, // пока эта операция не завершится. const file = fs.readFileSync(`${__dirname}/${filename}`); // Это ВСЕГДА будет выводится после завершения readFileSync. . . console.log("Done reading file."); // . . . А здесь ВСЕГДА будет выводится содержимое "file". console.log(`Contents: ${file.toString()}`);

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

// readfile_async.js "use strict"; // Этот пример из Node, поэтому не запускайте его в браузере. const filename = "text.txt", fs = require("fs"), getContents = function printContent (file) { try { return file.toString(); } catch (TypeError) { return file; } } console.log("Reading file . . . "); console.log("=".repeat(76)); // readFile выполняется АСИНХРОННО. // Программа продолжит выполнять то, что после LINE A // пока readFile делает свое дело. Мы скоро подробно обсудим // функции обратного вызова, пока просто обратите внимание на порядок логов let file; fs.readFile(`${__dirname}/${filename}`, function (err, contents) { file = contents; console.log(`Uh, actually, now I"m done. Contents are: ${ getContents(file) }`); }); // LINE A // Это ВСЕГДА будет выводится до завершения чтения файла. // Эти логи вводят в заблуждение и бесполезны. console.log(`Done reading file. Contents are: ${getContents(file)}`); console.log("=".repeat(76));

Основное преимущество синхронного кода состоит в том, что его проще читать и воспринимать: синхронные программы выполняются сверху вниз и строка n всегда завершается перед строкой n+1.

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

И именно поэтому JavaScript сделан неблокирующим в своей основе.

Вызов асинхронности

Переход к асинхронности дает нам скорость и забирает у нас линейность. Даже простой скрипт из примера выше демонстрирует это. Запомните:

  • Нет способа узнать, когда file будет доступен, кроме как передав управление readFile , который сообщит нам, когда все будет готово;
  • Наша программа больше не выполняется в том же порядке, в котором читаются, что усложняет их понимание.
  • Этих проблем достаточно, чтобы занять нас до конца статьи.

    Колбэки и фолбэки

    Попробуем немного упростить наш пример с readFile .

    "use strict"; const filename = "throwaway.txt", fs = require("fs"); let file, useless; useless = fs.readFile(`${__dirname}/${filename}`, function callback (error, contents) { file = contents; console.log(`Got it. Contents are: ${contents}`); console.log(`. . . But useless is still ${useless}.`); }); // Thanks to Rava for catching an error in this line. console.log(`File is ${useless}, but that"ll change soon.`);

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

    Вопрос в том, как мы можем узнать, что чтение завершено?

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

    Работает это примерно так: readFile смотрит,что находится внутри ${__dirname}/${filename} , а программа занимается своими делами. Как только readFile узнает, что там, он выполняет callback с contents в качестве аргумента, а в случае ошибки возвращает error .

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

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

    Функции обратного вызова это работающее решение, но не идеальное. У них есть две большие проблемы:

  • Инверсия управления
  • Сложная обработка ошибок
  • Инверсия контроля

    Первая проблема это проблема доверия.

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

    На практике это, конечно, не столь фатально: мы пишем функции обратного вызова почти 20 лет и до сих пор не поломали интернет. И в данном случае, мы знаем, что достаточно безопасно полагаться на код ядра Node.

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

    Неявная обработка ошибок

    В синхронном коде вы можете использовать try / catch / finally для обработки ошибок.

    "use strict"; // Этот пример из Node, поэтому не запускайте его в браузере. const filename = "text.txt", fs = require("fs"); console.log("Reading file . . . "); let file; try { // Неправильное имя файла. D"oh! file = fs.readFileSync(`${__dirname}/${filename + "a"}`); console.log(`Got it. Contents are: "${file}"`); } catch (err) { console.log(`There was a/n ${err}: file is ${file}`); } console.log("Catching errors, like a bo$$.");

    Асинхронный код пытается, конечно, но…

    "use strict"; // Этот пример из Node, поэтому не запускайте его в браузере. const filename = "throwaway.txt", fs = require("fs"); console.log("Reading file . . . "); let file; try { // Неправильное имя файла. D"oh! fs.readFile(`${__dirname}/${filename + "a"}`, function (err, contents) { file = contents; }); // Это не будет выполняться пока file равен undefined console.log(`Got it. Contents are: "${file}"`); } catch (err) { // В этом случае, catch должен запускаться, но это никогда не произойдет. // Это потому, что readFile передает ошибки в коллбэк, // а не возвращает. console.log(`There was a/n ${err}: file is ${file}`); }

    Это не работает так, как ожидается. Потому что блок try оборачивает readFile , который всегда успешно возвращает undefined . В такой ситуации у try всегда будет без происшествий.

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

    "use strict"; // Этот пример из Node, поэтому не запускайте его в браузере. const filename = "throwaway.txt", fs = require("fs"); console.log("Reading file . . . "); fs.readFile(`${__dirname}/${filename + "a"}`, function (err, contents) { if (err) { // catch console.log(`There was a/n ${err}.`); } else { // try console.log(`Got it. File contents are: "${file}"`); } });

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

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

    Промисы

    Представьте, вы только что заказали весь каталог You Don’t Know JS от O’Reilly. За ваши с трудом заработанные деньги они прислали расписку, что в следующий понедельник вы получите новенькую стопку книг. До этого счастливого понедельника никаких книг у вас не будет - но вы верите, что они появятся, так как вам пообещали (promise) прислать их.

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

    Конечно, через несколько дней O’Reilly может сообщить о том, что с понедельником не судьба и книги будут чуток позже, другими словами, нужное значение будет в будущем. Вы относитесь к промису, как к ожидаемому значению и пишете код так, как будто оно уже у вас есть.

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

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

    Жизненный цикл промиса: краткий обзор состояний

    Представьте, что вы используете промис для вызова API.

    Так как сервер не может ответить сразу, то и промис не может сразу содержать итоговое значение или отчет об ошибке. В таком состоянии промисы называются ожидающими (pending) . Этот тот же случай, что и ожидание стопки книг из нашего примера.

    Как только сервер ответил, у нас есть два возможных исхода:

  • Промис получает ожидаемое значение, значит, он выполнен (fulfilled) . Ваши книжки пришли.
  • Где-то по ходу выполнения произошла ошибка, промис отклонен (rejected) . Вы получили уведомление о том, что никаких книжек не будет.
  • Всего мы получаем три возможных состояния промиса , при этом состояния выполнения или отклонения не могут смениться другим состоянием.

    Теперь, когда мы разобрались с основными понятиями, посмотрим, как это все использовать.

    Фундаментальные методы промисов

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

    В этом разделе мы ближе рассмотрим базовое использование промисов:

  • Создание промисов с конструктором;
  • Обработка успешного результата с resolve ;
  • Обработка ошибок с reject ;
  • Настройка управления потоком с then и catch .
  • В нашем примере мы будем использовать промисы для очистки кода нашей функции fs.readFile .

    Создание промисов

    Самый простой способ это создание промисов непосредственно с помощью конструктора.

    "use strict"; const fs = require("fs"); const text = new Promise(function (resolve, reject) { // Does nothing })

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

  • Аргумент resolve это функция, инкапсулирующая то, что мы хотим сделать при получении ожидаемого значения . Когда мы получаем ожидаемое значение (val), мы передаем его resolve в качестве аргумента: resolve(val) .
  • Аргумент reject это тоже функция, представляющая наши действия в случае получения ошибки. Если мы получим ошибку (err), мы вызовем reject с ней: reject(err) .
  • Наконец, функция, переданная нами в конструктор промиса, обрабатывает сам асинхронный код. Если она возвращает ожидаемый результат, мы вызываем resolve с полученным значением. Если она выбрасывает ошибку, мы вызываем reject с этой ошибкой.
  • В нашем примере мы обернем fs.readFile в промис. Как должны выглядеть наши resolve и reject ?

  • При успехе мы хотим вызвать console.log для вывода содержимого файла.
  • При неудаче мы поступим аналогично: выведем ошибку в консоль.
  • Таким образом мы получим следующее:

    // constructor.js const resolve = console.log, reject = console.log;

    Затем нам надо написать функцию, которую мы передаем конструктору. Запомните, нам нужно сделать следующее:

  • Прочитать файл;
  • В случае успеха выполнить resolve с его содержимым;
  • При неудаче выполнить reject с полученной ошибкой.
  • Таким образом:

    // constructor.js const text = new Promise(function (resolve, reject) { // Normal fs.readFile call, but inside Promise constructor . . . fs.readFile("text.txt", function (err, text) { // . . . Call reject if there"s an error . . . if (err) reject(err); // . . . And call resolve otherwise. else // fs.readFile возвращает buffer, поэтому надо применить метод toString(). resolve(text.toString()); }) })

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

    Она дала обещание, а затем…

    Проблема в том, что мы написали наши методы resolve и reject , но на самом деле не передали их в промис. Для того, чтобы сделать это, нам надо ознакомиться с еще одной базовой функцией для управления потоком на основе промисов: then (затем).

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

    // constructor.js const text = new Promise(function (resolve, reject) { fs.readFile("text.txt", function (err, text) { if (err) reject(err); else resolve(text.toString()); }) }) .then(resolve, reject);

    Так промис прочитает файл и вызовет написанный нами метод resolve в случае успеха.

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

    Синтаксический сахар для обработки ошибок

    Мы передали then две функции: resolve , для вызова в случае успеха и reject на случай ошибки.

    У промисов также есть функция похожая на then , называемая catch . Она принимает обработчик reject в качестве единственного аргумента.

    Так как then всегда возвращает промис, в нашем примере мы можем только передать then обработчик resolve , а после этого подключить в цепочку catch с обработчиком reject .

    Const text = new Promise(function (resolve, reject) { fs.readFile("tex.txt", function (err, text) { if (err) reject(err); else resolve(text.toString()); }) }) .then(resolve) .catch(reject);

    Наконец, стоит упомянуть, что catch(reject) это всего лишь синтаксический сахар для then(undefined, reject) . То есть мы можем также написать:

    Const text = new Promise(function (resolve, reject) { fs.readFile("tex.txt", function (err, text) { if (err) reject(err); else resolve(text.toString()); }) }) .then(resolve) .then(undefined, reject);

    Но такой код будет менее читаемым.

    Заключение

    Промисы это незаменимый инструмент для асинхронного программирования. Они могут напугать поначалу, но только пока вы с ними незнакомы: используйте их пару раз и они станут такими же естественными для вас как if / else .

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

    В качестве дополнительной литературы ознакомьтесь со статьей Доменика Дениколы States and Fates , чтобы овладеть терминологией и с главой Кайла Симпсона о промисах из той стопки книг, на примере которой мы разбирали промисы.

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

    Выяснилось, что валидатор validator.w3.org вполне лояльно относится к синхронной версии кода счетчика и информера Яндекс.Метрики, но просто негодует, когда его заменяют асинхронными участками. Также выяснилось, что Яндекс.Метрика предоставляет пользователям (вебмастерам) возможность выбора синхронного или асинхронного вида счетчика среди прочих дополнительных настроек.

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

    ОЧЕНЬ ВАЖНО : скриншоты и повествование мы будем вести с использованием НОВОЙ Метрики от Яндекс. Это будет правильно, так как с 22 июня 2015 года данная версия будет основной, тогда как старый дизайн Метрики постепенно станет уходить в прошлое.

    Синхронный и асинхронный код счетчика Яндекс.Метрики: как поменять Шаг №1 Шаг №2

    Отыскиваем в списке своих сайтов ту площадку, код счетчика или информера которой нужно сменить, и запускаем функцию «Редактировать» (шестеренка с правой стороны – в новой версии Метрики).

    Шаг №3

    В опциях редактирования нас будет интересовать пункт меню «Код счетчика».

    Шаг №4

    Отыскиваем здесь функцию «асинхронный», на которой может стоять или отсутствовать галочка. Если такая галочка есть – вы видите асинхронный код счетчика в после ниже. Если же галочку снять – код счетчика Яндекс.Метрики станет стандартным – синхронным.

    Шаг №5

    Здесь же вы можете проверить и установить наличие или отсутствие ИНФОРМЕРА. Ранее мы говорили об этом в статье сайта « ». Так вот сейчас эти настройки выглядят в новой версии Метрики именно так и правятся именно здесь.

    Вот, собственно, и все. В любой момент вы можете:

    — сменить асинхронный код счетчика на синхронных;
    — установить обновленный и быстрый асинхронный код;
    — удалить или добавить информер счетчика Яндекс.Метрики.

    Обновленный код счетчика нужно вставить на сайт:

    Сегодня каждый день появляются новые языки программирования - 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.



    Понравилась статья? Поделиться с друзьями: