От автора: мы регулярно слышим заявления о том, что JavaScript асинхронный. Что это значит? Как это влияет на разработку? Как изменился подход в последние годы? Что такое в JavaScript потоки, и как ими управлять?
Рассмотрим следующий код:
result1 = doSomething1(); result2 = doSomething2(result1);
Большинство языков обрабатывают каждую строку синхронно. Первая строка запускается и возвращает результат. Вторая строка выполняется, как только первая завершилась независимо от того, сколько времени она занимает.
Однопоточная обработка
JavaScript работает на одном потоке обработки. При выполнении на вкладке браузера все остальное останавливается. Это необходимо, так как изменения на странице DOM не могут возникать при параллельных потоках; было бы опасно, чтобы один поток перенаправлялся на другой URL-адрес, а другой пытался добавить дочерние узлы.
Это редко проявляется пользователю, потому что обработка происходит быстро в небольших кусках. Например, JavaScript обнаруживает щелчок на кнопке, запускает расчет и обновляет DOM. После завершения браузер может обрабатывать следующий элемент в очереди.
(Замечание: другие языки, такие как PHP, также используют один поток, но могут управляться многопоточным сервером, таким как Apache. Два запроса на одну и ту же страницу PHP одновременно могут инициировать два потока с отдельными экземплярами PHP во время выполнения.)
Переход на асинхронность с помощью колбеков
Отдельные темы поднимают проблему. Что происходит, когда JavaScript вызывает «медленный» процесс, такой как запрос Ajax в браузере или операция базы данных на сервере? Эта операция может занять несколько секунд — даже минуты. Браузер закроется, пока он ждет ответа. На сервере приложение Node.js не сможет обрабатывать дальнейшие пользовательские запросы.
Решение представляет собой асинхронную обработку. Вместо того, чтобы ждать завершения, процессу предлагается вызвать другую функцию, когда результат будет готов. Это называется колбеком, и он передается как аргумент любой асинхронной функции. Например:
doSomethingAsync(callback1); console.log('finished'); // call when doSomethingAsync completes function callback1(error) { if (!error) console.log('doSomethingAsync complete'); }
doSomethingAsync() принимает колбек-функцию в качестве параметра (передается только ссылка на эту функцию, поэтому накладные расходы небольшие). Неважно, сколько времени doSomethingAsync() ; все, что мы знаем, это то, что callback1() будет выполнен в какой-то момент в будущем. Консоль покажет:
finished doSomethingAsync complete
Обратный звонок
Часто колбек вызван только одной асинхронной функцией. Поэтому можно использовать сжатые, анонимные встроенные функции:
doSomethingAsync(error => { if (!error) console.log('doSomethingAsync complete'); });
Ряд двух или более асинхронных вызовов может быть завершен последовательно, вставляя колбек-функции. Например:
async1((err, res) => { if (!err) async2(res, (err, res) => { if (!err) async3(res, (err, res) => { console.log('async1, async2, async3 complete.'); }); }); });
К сожалению, это вводит callback hell — известную концепцию, которая даже имеет свою собственную веб-страницу! Код трудно прочитать, и он будет ухудшаться при добавлении логики обработки ошибок.
Callback hell относительно редко встречается при кодировании на стороне клиента. Он может пройти два или три уровня в глубину, если вы делаете вызов Ajax, обновляете DOM и ожидаете завершения анимации, но обычно он остается управляемым.
Ситуация отличается от ОС или серверных процессов. Вызов API Node.js может получать загрузку файлов, обновлять несколько таблиц базы данных, записывать в журналы и делать дополнительные вызовы API перед отправкой ответа.
Promises
ES2015 (ES6) представил promises. Колбеки по-прежнему используются под поверхностью, но Promises обеспечивают более четкий синтаксис, который объединяет асинхронные команды, поэтому они запускаются последовательно (подробнее об этом в следующем разделе).
Чтобы включить выполнение на основе Promise, асинхронные функции, основанные на колбеках, должны быть изменены, чтобы они немедленно возвращали объект Promise. Этот объект обещает запустить одну из двух функций (переданных в качестве аргументов) в какой-то момент в будущем:
resolve : функция обратного вызова запускается при успешной завершении обработки и
reject : необязательная функция обратного вызова запускается при возникновении сбоя.
В приведенном ниже примере API базы данных предоставляет метод connect() который принимает колбек-функцию. Внешняя asyncDBconnect() немедленно возвращает новый promise и запускает либо resolve() либо reject() после установления соединения или сбоя:
const db = require('database'); // connect to database function asyncDBconnect(param) { return new Promise((resolve, reject) => { db.connect(param, (err, connection) => { if (err) reject(err); else resolve(connection); }); }); }
Node.js 8.0+ предоставляет утилиту util.promisify () для преобразования функции, основанной на обратном вызове, в альтернативу на основе Promise. Есть несколько условий:
колбек должен быть передан как последний параметр для асинхронной функции и
колбек-функция должна ожидать ошибку, за которой следует параметр значения.
Пример:
// Node.js: promisify fs.readFile const util = require('util'), fs = require('fs'), readFileAsync = util.promisify(fs.readFile); readFileAsync('file.txt');
Различные библиотеки на стороне клиента также предоставляют варианты promisify, но вы можете создать их самостоятельно в нескольких строках:
// promisify a callback function passed as the last parameter // the callback function must accept (err, data) parameters function promisify(fn) { return function() { return new Promise( (resolve, reject) => fn( ...Array.from(arguments), (err, data) => err ? reject(err) : resolve(data) ) ); } } // example function wait(time, callback) { setTimeout(() => { callback(null, 'done'); }, time); } const asyncWait = promisify(wait); ayscWait(1000);
Асинхронная сцепка
Все, что возвращает Promise, может начать серию асинхронных вызовов функций, определенных в методах .then() . Каждому передается результат из предыдущего resolve:
asyncDBconnect('http://localhost:1234') .then(asyncGetSession) // passed result of asyncDBconnect .then(asyncGetUser) // passed result of asyncGetSession .then(asyncLogAccess) // passed result of asyncGetUser .then(result => { // non-asynchronous function console.log('complete'); // (passed result of asyncLogAccess) return result; // (result passed to next .then()) }) .catch(err => { // called on any reject console.log('error', err); });
Синхронные функции также могут выполняться в блоках .then(). Возвращаемое значение передается следующему .then() (если есть).
Метод .catch() определяет функцию, вызываемую при .catch() любого предыдущего reject. В этот момент не будут выполняться дальнейшие методы .then(). У вас может быть несколько .catch() в цепочке для обнаружения различных ошибок.
ES2018 вводит метод .finally(), который выполняет любую конечную логику независимо от результата — например, для очистки, закрытия соединения с базой данных и т. д. В настоящее время он поддерживается только в Chrome и Firefox, но Technical Committee 39 выпустил .finally() полифил.
function doSomething() { doSomething1() .then(doSomething2) .then(doSomething3) .catch(err => { console.log(err); }) .finally(() => { // tidy-up here! }); }
Несколько асинхронных вызовов с Promise.all ()
Методы Promise .then() запускают асинхронные функции один за другим. Если порядок не имеет значения — например, инициализация несвязанных компонентов — быстрее запускается все асинхронные функции одновременно и завершается, когда выполняется последняя (самая медленная) функция.
Это может быть достигнуто с помощью Promise.all(). Он принимает множество функций и возвращает другой promise. Например:
Promise.all([ async1, async2, async3 ]) .then(values => { // array of resolved values console.log(values); // (in same order as function array) return values; }) .catch(err => { // called on any reject console.log('error', err); });
Promise.all() немедленно прекращается, если какая-либо из асинхронных функций вызывает reject.
Несколько асинхронных вызовов с Promise.race ()
Promise.race() похож на Promise.all() , за исключением того, что он разрешит или отклонит, как только первое Promise разрешит или отклонит. Только самая быстрая асинхронная функция на основе Promise будет когда-либо завершена:
Promise.race([ async1, async2, async3 ]) .then(value => { // single value console.log(value); return value; }) .catch(err => { // called on any reject console.log('error', err); });
Перспективное будущее?
Promises уменьшают callback hell, но представляют свои проблемы.
В учебниках часто не упоминается, что целая цепочка Promise асинхронна. Любая функция, использующая ряд promises, должна либо возвращать свои собственные promise, либо выполнять функции обратного вызова в конечных .catch() .then(), .catch() или .finally().
У меня также есть признание: promises смущали меня в течение долгого времени. Синтаксис часто кажется более сложным, чем обратные вызовы, есть много ошибок, и отладка может быть проблематичной. Тем не менее, важно изучить основы.
Async / Await
Promises могут быть сложными, поэтому ES2017 представил async и await. Хотя это может быть только синтаксический сахар, он обещает быть намного слаще, и вы можете избежать .then() цепей вообще. Рассмотрим пример на основе promises:
function connect() { return new Promise((resolve, reject) => { asyncDBconnect('http://localhost:1234') .then(asyncGetSession) .then(asyncGetUser) .then(asyncLogAccess) .then(result => resolve(result)) .catch(err => reject(err)) }); } // run connect (self-executing function) (() => { connect(); .then(result => console.log(result)) .catch(err => console.log(err)) })();
Чтобы переписать это с помощью async / await:
внешней функции должно предшествовать инструкция async и
вызовам асинхронных функций на основе Promise должно предшествовать await чтобы завершение обработки завершилось до выполнения следующей команды.
async function connect() { try { const connection = await asyncDBconnect('http://localhost:1234'), session = await asyncGetSession(connection), user = await asyncGetUser(session), log = await asyncLogAccess(user); return log; } catch (e) { console.log('error', err); return null; } } // run connect (self-executing async function) (async () => { await connect(); })();
await что каждый вызов выглядит так, как будто он синхронный, но не поддерживает одиночный поток обработки JavaScript. Кроме того, async функции всегда возвращают Promise, поэтому они, в свою очередь, могут быть вызваны другими функциями async.
код async / await не может быть короче, но есть значительные преимущества:
Синтаксис чище. Меньше скобок и меньше шансов ошибиться.
Отладка проще. Точки останова могут быть установлены в любом await утверждении.
Обработка ошибок лучше. Блоки try / catch могут использоваться так же, как и синхронный код.
Поддержка хорошая. Он реализован во всех браузерах (кроме IE и Opera Mini) и Node 7.6+.
Тем не менее, не все идеально …
Promises, Promises
async / await все еще полагается на Promises, которые в конечном итоге полагаются на колбеки. Вам нужно понять, как работают Promises, и нет прямого эквивалента Promise.all() и Promise.race(). Легко забыть о Promise.all(), который более эффективен, чем использование нескольких несвязанных команд await.
Асинхронные ожидания в синхронных циклах
В какой-то момент вы попытаетесь вызвать асинхронную функцию внутри синхронного цикла. Например:
async function process(array) { for (let i of array) { await doSomething(i); } }
Это не сработает. Также это не будет:
async function process(array) { array.forEach(async i => { await doSomething(i); }); }
Сами циклы остаются синхронными и всегда будут выполняться перед их внутренними асинхронными операциями.
ES2018 представляет асинхронные итераторы, которые похожи на обычные итераторы, за исключением того, что метод next() возвращает Promise. Поэтому ключевое слово await можно использовать for … of циклов для последовательного запуска асинхронных операций. например:
async function process(array) { for await (let i of array) { doSomething(i); } }
Однако до тех пор, пока не будут реализованы асинхронные итераторы, возможно, лучше всего map элементы массива с функцией async и запустить их с помощью Promise.all(). Например:
const todo = ['a', 'b', 'c'], alltodo = todo.map(async (v, i) => { console.log('iteration', i); await processSomething(v); }); await Promise.all(alltodo);
Это дает возможность запускать задачи параллельно, но невозможно передать результат одной итерации другой, а отображение больших массивов может быть дорогостоящим.
try/catch безобразие
async функции будут тихо выходить, если вы опустите try / catch любой await который терпит неудачу. Если у вас есть длинный набор асинхронных await команд, вам может потребоваться несколько блоков try / catch.
Одним из вариантов является функция более высокого порядка, которая улавливает ошибки, поэтому блоки try / catch становятся ненужными (спасибо @wesbos за предложение):
async function connect() { const connection = await asyncDBconnect('http://localhost:1234'), session = await asyncGetSession(connection), user = await asyncGetUser(session), log = await asyncLogAccess(user); return true; } // higher-order function to catch errors function catchErrors(fn) { return function (...args) { return fn(...args).catch(err => { console.log('ERROR', err); }); } } (async () => { await catchErrors(connect)(); })();
Однако этот вариант может быть непрактичным в ситуациях, когда приложение должно реагировать на некоторые ошибки другим способом от других.
Несмотря на некоторые подводные камни, async / await является элегантным дополнением к JavaScript.
Путешествие по JavaScript
Асинхронное программирование — задача, которую невозможно избежать в JavaScript. Колбеки важны в большинстве приложений, но их легко запутать в глубоко вложенных функциях.
Promises абстрагируют колбеки, но есть много синтаксических ловушек. Преобразование существующих функций может быть сложной, а цепи .then() все еще выглядят беспорядочными.
К счастью, async / await обеспечивает ясность. Код выглядит синхронно, но он не может монополизировать один поток обработки. Это изменит способ написания JavaScript и даже заставит вас оценить promises — если вы этого не сделали раньше!
Автор: Craig Buckler
Источник: https://www.sitepoint.com/
Редакция: Команда webformyself.