7 причин почему JavaScript Async/Await вытесняет прочь промисы (Руководство)
Async/await был представлен в NodeJS 7.6 и в настоящее время поддерживается всеми современными браузерами. Я верю, что это единственное грандиозное нововведение в JS с 2017 года. Если вы в замешательстве, вот несколько причин с примерами, почему вы должны принять его немедленно и никогда не оглядываться назад.
Async/Await 101
Для тех, кто еще не слышал об Async/Await ранее, приведено быстрое введение:
- Async/await — это новый способ написания асинхронного кода. Ранее его альтернативами были коллбэки и промисы.
- Async/await — это лишь синтаксический сахар, покрывающий промисы. Он не может быть использован вместе с коллбэками.
- Async/await, как и промисы, является неблокирующей функцией.
- Async/await заставляет асинхронный код выглядить и вести себя более, как синхронный код. Именно в этом заключается вся сила.
Синтаксис
Предположим есть функция getJSON, которая возвращает промис, и этот промис выполнится с некоторым JSON объектом. Мы хотим вызвать функцию и вывести этот JSON, а затем вернуть “done”.
Вот так бы мы реализовали ее, используя промисы
const makeRequest = () =>
getJSON()
.then(data => console.log(data)
return "done"
>)
makeRequest()
И вот как будет выглядеть реализация с использованием async/await
const makeRequest = async () => console.log(await getJSON())
return "done"
>
makeRequest()
Здесь имеется несколько отличий:
1. Перед функцией стоит ключевое слово async. Await может быть использован только внутри функции, определенной с использованием async. Любая async/await функция неявно возвращает промис, и результатом будет значение выполненного промиса, что бы мы не возвращали из функции (например, строку “done” в нашем случае).
2. Пункт выше подразумевает, что мы не можем использовать await на верхнем уровне нашего кода, поскольку он не находится внутри async функции.
// this will not work in top level
// await makeRequest()
// this will work
makeRequest().then((result) => // do something
>)
3. await getJSON() означает, что вызов console.log будет ждать пока getJSON() промис выполнится и выведет значение.
Почему это лучше?
1. Коротко и чисто
Взгляните, сколько коды мы не написали! Даже в замудренном примере выше видно, какое мы сократили количество кода. Нам не нужно писать .then, создавать анонимную функцию, чтобы обработать ответ, или давать название переменной, которую нам не нужно использовать. Мы также избежали вложенности кода. Эти небольшие преимущества станут более очевидным в следующих примерах кода.
2. Обработка ошибок
Async/await наконец-то делает возможным обрабатывать синхронные и асинхронные ошибки с той же конструкцией, старым добрым try/catch. В примере c промисами ниже, the try/catch не обработает, если JSON.parse не выполнится, так как это происходит внутри промиса. Нам нужно вызвать .catch промиса и продублировать код по обработке ошибки, который (надеюсь) будет более усложненный, чем console.log в вашем готовом коде.
const makeRequest = () => try getJSON()
.then(result => // this parse may fail
const data = JSON.parse(result)
console.log(data)
>)
// uncomment this block to handle asynchronous errors
// .catch((err) => // console.log(err)
// >)
> catch (err) console.log(err)
>
>
Теперь рассмотрим такой же код, написанный с помощью async/await. Блок catch теперь будет обрабатывать ошибки парсинга.
const makeRequest = async () => try // this parse may fail
const data = JSON.parse(await getJSON())
console.log(data)
> catch (err) console.log(err)
>
>
3. Условия
Ниже представлен код, в котором получены какие-то данные, и решается — стоит вернуть их, или получить детали, из значений полученных данных.
const makeRequest = () => return getJSON()
.then(data => if (data.needsAnotherRequest) return makeAnotherRequest(data)
.then(moreData => console.log(moreData)
return moreData
>)
> else console.log(data)
return data
>
>)
>
Рассматривая этот код, можно обзавестись головной болью. Легко запутаться в приведенных вложенностях (6 уровней), скобках и возвращаемых значениях, которые нужны только для поднятия окончательного результата до главного промиса.
Пример ниже становится более наглядным, после переписывания на async/await.
const makeRequest = async () => const data = await getJSON()
if (data.needsAnotherRequest) const moreData = await makeAnotherRequest(data);
console.log(moreData)
return moreData
> else console.log(data)
return data
>
>
4. Промежуточные значения
Вы, вероятно, оказывались в ситуации, когда нужно вызвать promise1, и затем использовать, возвращаемое значение, чтобы вызвать promise2, а затем использовать результат обоих промисов, чтобы вызвать promise3. Ваш код, вероятнее всего, был похож на следующий
const makeRequest = () => return promise1()
.then(value1 => // do something
return promise2(value1)
.then(value2 => // do something
return promise3(value1, value2)
>)
>)
>
Если promise3 не требует value1, будет немного проще упростить вложенность. Если вы из тех людей,которых не устроит подобный код, вы можете обернуть value1 и value2 в Promise.all, и таким образом, получить вложенность меньше, вот так
const makeRequest = () => return promise1()
.then(value1 => // do something
return Promise.all([value1, promise2(value1)])
>)
.then(([value1, value2]) => // do something
return promise3(value1, value2)
>)
>
Такой подход жертвует семантикой ради читабельности кода. Нет другой причины помещать value1 & value2 в массив вместе, кроме избежания вложенности промисов.
Та же логика становится смехотворно простой и интуитивно понятной с async/await. Она заставляет задуматься обо всех вещах, которые вы могли бы сделать за то время, которое потратили, пытаясь сделать промисы менее жуткими.
const makeRequest = async () => const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
>
5. Ошибки
Представьте участок кода, который вызывает множественную цепочку промисов, и где-то внутри цепи выбрасывается ошибка.
const makeRequest = () => return callAPromise()
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => throw new Error("oops");
>)
>
makeRequest()
.catch(err => console.log(err);
// output
// Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
>)
Стек ошибок, возвращаемый из цепочки промисов, не дает представления о том, в каком месте произошла ошибка. Хуже того, это вводит в заблуждение; единственное имя функции, которое она содержит, — это callAPromise, который абсолютно не причем к этой ошибке (хотя файл и номер строки по-прежнему полезны).
Однако стек ошибок из async/await указывает на функцию, которая содержит ошибку
const makeRequest = async () => await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
throw new Error("oops");
>
makeRequest()
.catch(err => console.log(err);
// output
// Error: oops at makeRequest (index.js:7:9)
>)
Это не слишком большое преимущество, когда вы пишите код в своей локальной среде и открываете файл в редакторе, но это весьма полезно, когда вы пытаетесь разобраться в журналах ошибок, поступающих с вашего производственного сервера. В таких случаях лучше знать, что ошибка произошла в makeRequest, чем знать, что ошибка возникла из-за-за-за…
6. Отладка
Убийственным преимуществом использования async/await является простая отладка. Отлаживание промисов всегда было болезненным процессом по 2 причинам
- Вы можете установить брейкпоинт в стрелочной функции, который вернет выражения (без тела).
2. Если вы установите брейкпоинт внутри блока .then, отладчик не будет переходить к следующему .then, так как он “шагает” только по синхронному коду.
С async/await вам больше не нужны стрелочные функции, и вы можете выполнить шаг через await так, как если бы это были обычные синхронные вызовы.
7. Вы можете подождать, что угодно
И, наконец, await может применяться как для синхронных, так и асинхронных выражений. Например, вы можете написать await 5, что будет эквивалентно Promise.resolve(5). На первый взгляд, такая запись не выглядит весьма полезной, но станет большим преимуществом, когда при написании библиотеки или вспомогательной функции, вы не знаете будет ли она использоваться как синхронная или асинхронная.
Представьте, что вы хотите записать время, необходимое для выполнения некоторых вызовов API в вашем приложении, и решили создать для этой цели универсальную функцию. Вот как это будет выглядеть с промисами
const recordTime = (makeRequest) => const timeStart = Date.now();
makeRequest().then(() => < // throws error for sync functions (.then is not a function)
const timeEnd = Date.now();
console.log('time take:', timeEnd - timeStart);
>)
>
Известно, что все API вызовы возвращают промис, но что произойдет, если вы используете ту же функцию для записи времени, затраченного на синхронную функцию? Будет выброшена ошибка, потому что синхронная функция не возвращает промис. Обычным способом избежать такого поведения является обертывание makeRequest () в Promise.resolve ().
Если вы используете async/await, не стоит волноваться о таких случаях, так как await безопасно отработает с любым значением
const recordTime = async (makeRequest) => const timeStart = Date.now();
await makeRequest(); // works for any sync or async function
const timeEnd = Date.now();
console.log('time take:', timeEnd - timeStart);
>
В заключение
Async/await — одно из революционных нововведений, добавленных в JavaScript за последние несколько лет. Это позволяет вам понять, что такое синтаксический беспорядок, и обеспечивает интуитивно понятную и достойную замену.
Выводы
Некоторый скептицизм, который может присутствовать в использовании async/await, заключается в том, что он делает асинхронный код менее очевидным: мы научились распознавать асинхронный код всякий раз, когда мы видим функцию обратного вызова (callback) или .then. Теперь нам потребуется время, чтобы приспособиться к новым признакам асинхронного кода, но в C# такая функция существует в течение многих лет, и люди, которые знакомые с данным языком программирования, знают, что это небольшое, временное неудобство.
Promise.prototype.then()
Метод then() объекта Promise принимает до двух аргументов: функции обратного вызова для выполненных и отклоненных случаев Promise . Он немедленно возвращает эквивалентный объект Promise , позволяя вам chain вызывать другие методы promise.
Try it
Syntax
then(onFulfilled) then(onFulfilled, onRejected)
Parameters
onFulfilled Optional
Функция для асинхронного выполнения при выполнении этого promise. Его возвращаемое значение становится значением выполнения promise, возвращаемым then() . Функция вызывается со следующими аргументами:
Значение, с которым был выполнен promise.
Если это не функция, она внутренне заменяется функцией идентификации ( (x) => x ), которая просто передает значение выполнения вперед.
Функция для асинхронного выполнения, когда этот promise будет отклонен. Его возвращаемое значение становится значением выполнения promise, возвращаемым catch() . Функция вызывается со следующими аргументами:
Значение, с которым был отклонен promise.
Если это не функция, она внутренне заменяется функцией метателя ( (x) => < throw x; >), которая выдает полученную причину отклонения.
Return value
Немедленно возвращает новый Promise . Этот новый promise всегда ожидает возврата, независимо от текущего статуса promise.
Один из обработчиков onFulfilled и onRejected будет выполняться для обработки выполнения или отклонения текущего promise. Вызов всегда происходит асинхронно, даже если текущий promise уже урегулирован. Поведение возвращенного promise (назовем его p ) зависит от результата выполнения обработчика в соответствии с определенным набором правил. Если функция обработчика:
- возвращает значение: p выполняется с возвращенным значением в качестве значения.
- ничего не возвращает: p выполняется со значением undefined .
- выдает ошибку: p отклоняется с выброшенной ошибкой в качестве значения.
- возвращает уже выполненный promise: p выполняется с этим значением promise в качестве значения.
- возвращает уже отклоненный promise: p отклоняется со значением этого promise в качестве значения.
- возвращает другой ожидающий promise: p находится в ожидании и становится fulfilled/rejected со значением этого promise в качестве значения сразу после того, как promise становится fulfilled/rejected.
Description
Метод then() планирует функции обратного вызова для возможного завершения Promise — либо выполнения, либо отклонения. Это примитивный метод promises: протокол thenable ожидает, что все объекты, подобные promise, будут предоставлять метод then() , а методы catch() и finally() работают, вызывая метод объекта then() .
Дополнительные сведения об обработчике onRejected см. в справочнике catch() .
then() возвращает новый объект promise. Если дважды вызвать метод then() для одного и того же объекта promise (вместо chaining),), то этот объект promise будет иметь две пары обработчиков расчетов. Все обработчики, прикрепленные к одному и тому же объекту promise, всегда вызываются в том порядке, в котором они были добавлены. Более того, два promise, возвращаемые каждым вызовом then() , запускают отдельные цепочки и не ждут расчетов друг друга.
Объекты Thenable , возникающие в цепочке then() , всегда имеют статус resolved — обработчик onFulfilled никогда не получает объект thenable, а любой объект thenable, возвращаемый любым обработчиком, всегда разрешается перед передачей следующему обработчику. Это связано с тем, что при построении нового promise функции resolve и reject , переданные executor , сохраняются, и когда текущий promise устанавливается, соответствующая функция будет вызываться со значением выполнения или причиной отказа. Логика разрешения исходит от функции разрешения, переданной конструктором Promise() .
then() поддерживает создание подклассов, что означает, что его можно вызывать для экземпляров подклассов Promise , и результатом будет promise типа подкласса. Вы можете настроить тип возвращаемого значения с помощью свойства @@species .
Examples
Используя метод then()
const p1 = new Promise((resolve, reject) => < resolve("Success!"); // or // reject(new Error("Error!")); >); p1.then( (value) => < console.log(value); // Success! >, (reason) => < console.error(reason); // Error! >, );
Наличие нефункции в качестве любого параметра
Promise.resolve(1).then(2).then(console.log); // 1 Promise.reject(1).then(2, 2).then(console.log, console.log); // 1
Chaining
Метод then возвращает новый Promise , что позволяет создавать цепочки методов.
Если функция, переданная в качестве обработчика then , возвращает Promise , эквивалентный Promise будет открыт для последующего then в цепочке методов. Фрагмент ниже имитирует асинхронный код с помощью функции setTimeout .
Promise.resolve("foo") // 1. Получите "foo", соедините с ним "bar" и разрешите его следующему, затем .then( (string) => new Promise((resolve, reject) => < setTimeout(() => < string += "bar"; resolve(string); >, 1); >), ) // 2. получить "foobar", зарегистрировать функцию обратного вызова для работы с этой строкой // и вывести в консоль, но не раньше, чем вернуть необработанный // строка к следующей then .then((string) => < setTimeout(() => < string += "baz"; console.log(string); // foobarbaz >, 1); return string; >) // 3. вывести полезные сообщения о том, как будет выполняться код в этом разделе // до того, как строка будет фактически обработана фиктивным асинхронным кодом в // предыдущий, затем блок. .then((string) => < console.log( "Last Then: oops. didn't bother to instantiate and return a promise in the prior then so the sequence may be a bit surprising", ); // Обратите внимание, что в этом месте `string` не будет бита 'baz'. Этот // потому что мы смоделировали, чтобы это происходило асинхронно с функцией setTimeout console.log(string); // foobar >); // Logs, in order: // Last Then: oops. не удосужился создать экземпляр и вернуть promise до этого, поэтому последовательность может быть немного неожиданной // foobar // foobarbaz
Значение, возвращаемое из then() , разрешается так же, как и Promise.resolve() . Это означает, что thenable objects поддерживаются, и если возвращаемое значение не является promise, оно неявно заворачивается в Promise , а затем разрешается.
const p2 = new Promise((resolve, reject) => < resolve(1); >); p2.then((value) => < console.log(value); // 1 return value + 1; >).then((value) => < console.log(value, "- A synchronous value works"); // 2 - работает синхронное значение >); p2.then((value) => < console.log(value); // 1 >);
Вызов then возвращает promise, который в конечном итоге отклоняется, если функция выдает ошибку или возвращает отклоненный Promise.
Promise.resolve() .then(() => < // Makes .then() return a rejected promise throw new Error("Oh no!"); >) .then( () => < console.log("Not called."); >, (error) => < console.error(`onRejected function called: $ `); >, );
На практике часто желательно, чтобы catch() отклонял promises, а не двухрегистровый синтаксис then() , как показано ниже.
Promise.resolve() .then(() => < // Заставляет .then() вернуть отклоненный promise throw new Error("Oh no!"); >) .catch((error) => < console.error(`onRejected function called: $ `); >) .then(() => < console.log("I am always called even if the prior then's promise rejects"); >);
Во всех остальных случаях возвращенный promise в конечном итоге отрабатывает. В следующем примере первый then() возвращает 42 , завернутый в выполненный Promise, несмотря на то, что предыдущий Promise в цепочке был отклонен.
Promise.reject() .then( () => 99, () => 42, ) // onRejected возвращает 42, завернутый в выполненный Promise .then((solution) => console.log(`Resolved with $ `)); // Выполнено с 42
Если onFulfilled возвращает promise, возвращаемое значение then будет fulfilled/rejected в зависимости от конечного состояния этого promise.
function resolveLater(resolve, reject) < setTimeout(() => < resolve(10); >, 1000); > function rejectLater(resolve, reject) < setTimeout(() => < reject(new Error("Error")); >, 1000); > const p1 = Promise.resolve("foo"); const p2 = p1.then(() => < // Возвращаем здесь promise, который будет преобразован в 10 через 1 секунду return new Promise(resolveLater); >); p2.then( (v) => < console.log("resolved", v); // "решено", 10 >, (e) => < // not called console.error("rejected", e); >, ); const p3 = p1.then(() => < // Вернуть сюда promise, который будет отклонен с помощью 'Error' через 1 секунду return new Promise(rejectLater); >); p3.then( (v) => < // не вызывается console.log("resolved", v); >, (e) => < console.error("rejected", e); // "отклонено", 'Error' >, );
Вы можете использовать цепочку для реализации одной функции с API на базе Promise поверх другой такой функции.
function fetchCurrentData( ) < // fetch() API возвращает Promise. Эта функция // выставляет аналогичный API,, за исключением выполнения // значение Promise этой функции было больше // проделанная над ним работа. return fetch("current-data.json").then((response) => < if (response.headers.get("content-type") !== "application/json") < throw new TypeError(); > const j = response.json(); // возможно, сделать что-то с j // значение выполнения, данное пользователю // fetchCurrentData().then() return j; >); >
Асинхронность then()
Ниже приведен пример, демонстрирующий асинхронность метода then .
// Например, используя разрешенный promise 'resolvedProm', // вызов функции 'resolvedProm.then(. )' немедленно возвращает новый promise, // но его обработчик '(value) => ' будет вызываться асинхронно, как показано на console.logs. // новый promise назначается 'thenProm', // и thenProm будут разрешены со значением, возвращаемым обработчиком const resolvedProm = Promise.resolve(33); console.log(resolvedProm); const thenProm = resolvedProm.then((value) => < console.log( `this gets called after the end of the main stack. the value received is: $ , the value returned is: $< value + 1 >`, ); return value + 1; >); console.log(thenProm); // С помощью setTimeout, мы можем отложить выполнение функции до момента, когда стек опустеет setTimeout(() => < console.log(thenProm); >); // Журналы по порядку: // Promise // Promise // "это вызывается после окончания основного стека. полученное значение: 33, возвращенное значение: 34" // Promise
Specifications
Specification |
---|
ECMAScript Спецификация языка # sec-promise.prototype.then |
Browser compatibility
Desktop | Mobile | Server | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Chrome | Edge | Firefox | Internet Explorer | Opera | Safari | WebView Android | Chrome Android | Фаерфокс для Android | Opera Android | Сафари на IOS | Samsung Internet | Deno | Node.js | |
then | 32 | 12 | 29 | No | 19 | 8 | 4.4.3 | 32 | 29 | 19 | 8 | 2.0 | 1.0 | 0.12.0 |
Для чего в промисах используются return?
Объясните, пожалуйста, для чего вообще в промисах используются return’ы? Например, вот в этих ситуациях:
fsp.readFile('./first', 'utf-8') .then((data1) => console.log(data1); // Читаем файл и продолжаем промис от этой внутренней функции return fsp.readFile('./second', 'utf-8').then((data2) => console.log(data2); // Читаем файл и продолжаем промис от этой внутренней функции return fsp.readFile('./third', 'utf-8').then((data3) => console.log(data3); >); >); >);
fs.readFile(filename, 'utf-8') .then((content) => return fs.unlink(filename).then(() => return content; >); >) .then((content) => // где-то тут создаем новый файл, с обновленным контентом >);
- Количеству голосов ▼
- Дата создания
08 декабря 2022
В общем случае в промисах есть два варианта использования return:
- используется для возврата значения из промиса. В асинхронных запросах на колбеках передаются функции-колбеки, в которые передается результат. В промисах же колбека нет. Чтобы использовать результат асинхронной функции в промисах, возвращается (return) нужный результат. Этот возвращаемый результат попадет в следующую цепочку then
- второй случай — это когда возвращается сам промис. Возвращать промис нужно, чтобы следующая цепочка then могла выполнить этот промис и получить его результат. Например, есть функция внутри которой используется промис и он не возвращается:
const func = () => const promise = new Promise((resolve, reject) => setTimeout(() => console.log('promise done!'); resolve('result from promise'); >, 1000); >); >;
то вызывая такую функцию, нет возможности получить результат асинхронной функции. Невозможно даже контролировать выполнение этой функции:
console.log('begin'); func(); console.log('end'); // begin // end // promise done!
В первом примере return используется для возврата промиса, что бы следующая цепочка then получила его и выполнила.
Во втором пример, внутри первого then вызывается асинхронная функция, которая возвращает новый промис. На нем вызывается then — этот then ждет выполнения этого промиса и вызывает переданный колбек. Результат этого колбека передастся в следующую цепочку then. В данном случае это будет content.
Promise¶
Класс Promise существует во многих современных движках JavaScript и может быть легко заполифиллен. Основной причиной использования промисов является синхронный стиль обработки ошибок в отличие от асинхронного коллбэк стиля.
Коллбэк стиль¶
Чтобы в полной мере оценить промисы, давайте рассмотрим простой пример, который доказывает сложность создания надежного асинхронного кода с помощью только коллбэков. Рассмотрим простой случай создания асинхронной версии загрузки JSON из файла. Синхронная версия этого может быть довольно простой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import fs = require('fs'); function loadJSONSync(filename: string) return JSON.parse(fs.readFileSync(filename)); > // валидный json файл console.log(loadJSONSync('good.json')); // несуществующий файл, поэтому fs.readFileSync завершается ошибкой try console.log(loadJSONSync('absent.json')); > catch (err) console.log('absent.json error', err.message); > // невалидный json файл т.е файл существует, но содержит невалидный JSON, // поэтому JSON.parse завершается ошибкой try console.log(loadJSONSync('invalid.json')); > catch (err) console.log('invalid.json error', err.message); >
Эта простая функция loadJSONSync имеет три варианта поведения: валидное возвращаемое значение, ошибка файловой системы или ошибка JSON.parse. Мы обрабатываем ошибки с помощью простого метода try / catch, как вы привыкли делать синхронное программирование на других языках. Теперь давайте сделаем хорошую асинхронную версию такой функции. Неплохая первоначальная версия с простой проверкой ошибки будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12
import fs = require('fs'); // Неплохая первоначальная версия . но неправильная. Мы объясним причины ниже function loadJSON( filename: string, cb: (error: Error, data: any) => void ) fs.readFile(filename, function (err, data) if (err) cb(err); else cb(null, JSON.parse(data)); >); >
Всё достаточно просто, функция принимает коллбэк, передавая ему любые ошибки файловой системы. Если нет ошибок файловой системы, он возвращает результат JSON.parse . При работе с асинхронными функциями, основанными на обратных вызовах, следует помнить следующее:
- Никогда не вызывайте коллбэк дважды.
- Никогда не бросайте ошибку.
Однако эта простая функция не подходит для второго пункта. Фактически, JSON.parse выдает ошибку, если ему передан неверный JSON, в итоге коллбэк никогда не вызывается и приложение вылетает. Это продемонстрировано в следующем примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import fs = require('fs'); // Неплохая первоначальная версия . но неправильная function loadJSON( filename: string, cb: (error: Error, data: any) => void ) fs.readFile(filename, function (err, data) if (err) cb(err); else cb(null, JSON.parse(data)); >); > // загрузка невалидного json loadJSON('invalid.json', function (err, data) // Этот код никогда не выполнится if (err) console.log('bad.json error', err.message); else console.log(data); >);
Было бы наивной попыткой исправить это обернуть JSON.parse в try. catch , как показано в следующем примере:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
import fs = require('fs'); // Версия получше . но всё ещё неправильная function loadJSON( filename: string, cb: (error: Error) => void ) fs.readFile(filename, function (err, data) if (err) cb(err); > else try cb(null, JSON.parse(data)); > catch (err) cb(err); > > >); > // загрузка невалидного json loadJSON('invalid.json', function (err, data) if (err) console.log('bad.json error', err.message); else console.log(data); >);
Тем не менее, в этом коде есть небольшая ошибка. Если коллбэк ( cb ), а не JSON.parse , выдает ошибку, так как мы завернули ее в try. catch , выполняется catch , и мы снова вызываем коллбэк, т. е. вызываем дважды! Это продемонстрировано в примере ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
import fs = require('fs'); function loadJSON( filename: string, cb: (error: Error) => void ) fs.readFile(filename, function (err, data) if (err) cb(err); > else try cb(null, JSON.parse(data)); > catch (err) cb(err); > > >); > // валидный файл, но плохой коллбэк . вызывается снова! loadJSON('good.json', function (err, data) console.log('our callback called'); if (err) console.log('Error:', err.message); else // давайте имитируем ошибку, пытаясь получить // доступ к свойству неопределенной переменной var foo; // Следующий код выбросит ошибку // `Error: Cannot read property 'bar' of undefined` console.log(foo.bar); > >);
1 2 3 4
$ node asyncbadcatchdemo.js our callback called our callback called Error: Cannot read property 'bar' of undefined
Это потому что наша функция loadJSON неправильно завернула коллбэк в блок try . Здесь нужно запомнить простое правило.
Держите весь ваш синхронный код в try. catch , кроме случаев, когда вы вызываете коллбэк.
Следуя этому простому правилу, мы имеем полностью функциональную асинхронную версию loadJSON , как показано ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import fs = require('fs'); function loadJSON( filename: string, cb: (error: Error) => void ) fs.readFile(filename, function (err, data) if (err) return cb(err); // Держим весь ваш синхронный код в try catch try var parsed = JSON.parse(data); > catch (err) return cb(err); > // кроме случаев, когда вы вызываете коллбэк return cb(null, parsed); >); >
Конечно, этому легко следовать как только вы уже проделали это несколько раз, но, тем не менее, это много шаблонного кода, который нужно писать просто для хорошей обработки ошибок. Теперь давайте рассмотрим более удачный способ борьбы с асинхронным JavaScript с использованием промисов.
Создание Promise¶
Промис может быть в состоянии ожидание( pending ), исполнено( fulfilled ) или отклонено( rejected ).
Давайте посмотрим на создание промиса. Достаточно просто вызвать new с Promise (конструктор промисов). Конструктор промисов передаст resolve и reject функции для определения состояния промиса:
1 2 3
const promise = new Promise((resolve, reject) => // resolve / reject функции контролируют варианты завершения промиса >);
Подписка на варианты завершения промиса¶
На варианты завершения промиса можно подписаться с помощью .then (если исполнено) или .catch (если отклонено).
1 2 3 4 5 6 7 8 9
const promise = new Promise((resolve, reject) => resolve(123); >); promise.then((res) => console.log('I get called:', res === 123); // I get called: true >); promise.catch((err) => // Не будет вызвано >);
1 2 3 4 5 6 7 8 9 10
const promise = new Promise((resolve, reject) => reject(new Error('Something awful happened')); >); promise.then((res) => // Не будет вызвано >); promise.catch((err) => console.log('I get called:', err.message); // I get called: 'Something awful happened' >);
- Быстрое создание исполненного промиса: Promise.resolve(result)
- Быстрое создание отклоненного промиса: Promise.reject(error)
Цепочки промисов¶
Способность создавать цепочки вызова промисов — это главное преимущество, которое предоставляют промисы. Как только у вас появляется промис, вы можете использовать функцию then для создания цепочки промисов.
- Если вы возвращаете промис из любой функции в цепочке, .then вызывается только когда его значение resolved:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Promise.resolve(123) .then((res) => console.log(res); // 123 return 456; >) .then((res) => console.log(res); // 456 return Promise.resolve(123); // Обратите внимание, что мы возвращаем промис >) .then((res) => console.log(res); // 123 : Обратите внимание, что этот `then` // вызывается со значением resolved return 123; >);
- Вы можете объединить обработку ошибок любой предыдущей части цепочки с помощью одного catch :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// Создаём rejected промис Promise.reject(new Error('something bad happened')) .then((res) => console.log(res); // не вызывается return 456; >) .then((res) => console.log(res); // не вызывается return 123; >) .then((res) => console.log(res); // не вызывается return 123; >) .catch((err) => console.log(err.message); // случилось что-то плохое >);
- сatch фактически возвращает новый промис (фактически создавая новую цепочку промисов):
1 2 3 4 5 6 7 8 9 10 11 12 13
// Создаём rejected промис Promise.reject(new Error('something bad happened')) .then((res) => console.log(res); // не вызывается return 456; >) .catch((err) => console.log(err.message); // случилось что-то плохое return 123; >) .then((res) => console.log(res); // 123 >);
- Любые синхронные ошибки, добавленные в then (или catch ), приводят к тому, что возвращаемый промис не исполняется:
1 2 3 4 5 6 7 8 9 10 11 12 13
Promise.resolve(123) .then((res) => throw new Error('something bad happened'); // генерируем синхронную ошибку return 456; >) .then((res) => console.log(res); // не вызывается return Promise.resolve(789); >) .catch((err) => console.log(err.message); // случилось что-то плохое >);
- Только соответствующий (ближайший) catch вызывается для данной ошибки (так как catch запускает новую цепочку промисов).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Promise.resolve(123) .then((res) => throw new Error('something bad happened'); // генерируем синхронную ошибку return 456; >) .catch((err) => console.log('first catch: ' + err.message); // случилось что-то плохое return 123; >) .then((res) => console.log(res); // 123 return Promise.resolve(789); >) .catch((err) => console.log('second catch: ' + err.message); // не вызывается >);
- сatch вызывается только в случае ошибки в предыдущей цепочке:
1 2 3 4 5 6 7
Promise.resolve(123) .then((res) => return 456; >) .catch((err) => console.log('HERE'); // не вызывается >);
- ошибки переходят к catch (и пропускают любые средние вызовы then ) и
- синхронные ошибки также отлавливаются любым catch ,
эффективно предоставляя нам парадигму асинхронного программирования, которая позволяет обрабатывать ошибки лучше, чем необработанные коллбэки. Подробнее об этом ниже.
TypeScript и промисы¶
Отличительной особенностью TypeScript является то, что он понимает поток значений, проходящих через цепочку промисов:
1 2 3 4 5 6 7 8
Promise.resolve(123) .then((res) => // res подразумевает тип `number` return true; >) .then((res) => // res подразумевает тип `boolean` >);
Конечно, он также понимает развертывание любых вызовов функций, которые могут вернуть промис:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
function iReturnPromiseAfter1Second(): Promisestring> return new Promise((resolve) => setTimeout(() => resolve('Hello world!'), 1000); >); > Promise.resolve(123) .then((res) => // res подразумевает тип `number` return iReturnPromiseAfter1Second(); // мы возвращаем `Promise` >) .then((res) => // res подразумевает тип `string` console.log(res); // Hello world! >);
Преобразование коллбэка для возврата промиса¶
Просто оберните вызов функции в промис и
- reject если произошла ошибка,
- resolve если все хорошо.
Например, давайте завернем fs.readFile :
1 2 3 4 5 6 7 8 9
import fs = require('fs'); function readFileAsync(filename: string): Promiseany> return new Promise((resolve, reject) => fs.readFile(filename, (err, result) => if (err) reject(err); else resolve(result); >); >); >
Возвращаясь к примеру с JSON¶
Теперь давайте вернемся к нашему примеру loadJSON и перепишем асинхронную версию, которая использует промисы. Все, что нам нужно сделать, это прочитать содержимое файла как промис, затем парсим его как JSON, и все готово. Это показано в следующем примере:
1 2 3 4 5 6
function loadJSONAsync(filename: string): Promiseany> return readFileAsync(filename) // Use the function we just wrote .then(function (res) return JSON.parse(res); >); >
Использование (обратите внимание, насколько оно похоже на оригинальную версию sync , представленную в начале этого раздела ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
// валидный json файл loadJSONAsync('good.json') .then(function (val) console.log(val); >) .catch(function (err) console.log('good.json error', err.message); // не вызывается >) // несуществующий json файл .then(function () return loadJSONAsync('absent.json'); >) .then(function (val) console.log(val); >) // не вызывается .catch(function (err) console.log('absent.json error', err.message); >) // невалидный json файл .then(function () return loadJSONAsync('invalid.json'); >) .then(function (val) console.log(val); >) // не вызывается .catch(function (err) console.log('bad.json error', err.message); >);
Эта функция проще, потому что объединение промисов произвело » loadFile (async) + JSON.parse (sync) => catch » объединение. Также коллбэк не был вызван нами, но вызван цепочкой промисов, поэтому у нас не было возможности совершить ошибку, заключив его в try. catch .
Параллельное управление потоком¶
Мы видели, как просто выполнять серию последовательных асинхронных задач с промисами. Просто использованием цепочки вызовов then .
Однако вы, возможно, захотите выполнить серию асинхронных задач, а затем что-то сделать с результатами всех этих задач. Promise предоставляет статическую функцию Promise.all , которую вы можете использовать для ожидания выполнения n промисов. Вы предоставляете ему массив n промисов, и он дает вам массив n resolve значений. Ниже мы показываем последовательный вызов по цепочке, а также параллельно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
// асинхронная функция для имитации загрузки элемента с какого-либо сервера function loadItem(id: number): Promise id: number >> return new Promise((resolve) => console.log('loading item', id); setTimeout(() => // имитируем задержку сервера resolve( id: id >); >, 1000); >); > // последовательный вызов по цепочке let item1, item2; loadItem(1) .then((res) => item1 = res; return loadItem(2); >) .then((res) => item2 = res; console.log('done'); >); // общее время будет около 2с. // параллельный вызов Promise.all([loadItem(1), loadItem(2)]).then((res) => [item1, item2] = res; console.log('done'); >); // общее время будет около 1с.
Иногда вы хотите запустить серию асинхронных задач, но вы получите все, что вам нужно, при условии, что будет решена любая из этих задач. Promise предоставляет статическую функцию Promise.race для этого сценария:
1 2 3 4 5 6 7 8 9 10 11
var task1 = new Promise(function (resolve, reject) setTimeout(resolve, 1000, 'one'); >); var task2 = new Promise(function (resolve, reject) setTimeout(resolve, 2000, 'two'); >); Promise.race([task1, task2]).then(function (value) console.log(value); // "one" // исполнены обе, но task1 исполнился быстрее >);
Преобразование коллбэков в промис¶
Самый надежный способ сделать это вручную. Например, преобразовать setTimeout в промис delay очень просто:
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
Обратите внимание, что в NodeJS есть супер-удобная функция, которая выполняет вот такую node style function => promise returning function магию для вас:
1 2 3 4
/** Пример использования */ import fs = require('fs'); import util = require('util'); const readFile = util.promisify(fs.readFile);