Асинхронное программирование с async-await

в 4:55, , рубрики: async, async/await, javascript, асинхронность, Программирование, разработка, Разработка веб-сайтов

Доброго времени суток, друзья!

Сравнительно новыми дополнениями JavaScript являются асинхронные функции и ключевое слово await. Эти возможности в основном являются синтаксическим сахаром над обещаниями (промисами), облегчая написание и чтение асинхронного кода. Они делают асинхронный код похожим на синхронный. Данная статья поможет вам разобраться, что к чему.

Условия: базовая компьютерная грамотность, знание основ JS, понимание азов асинхронного кода и обещаний.
Цель: понять, как устроены обещания, и как они используются.

Основы async/await

Использование async/await состоит из двух частей.

Ключевое слово async

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

Попробуйте набрать в консоли браузера следующее:

function hello(){ return 'Hello' }
hello()

Функция вернет 'Hello'. Ничего необычно, верно?

Но что если мы превратим ее в асинхронную функцию? Попробуйте сделать следующее:

async function hello(){ return 'Hello' }
hello()

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

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

let hello = async function(){ return hello() }
hello()

Также можно использовать стрелочные функции:

let hello = async () => { return 'Hello' }

Все эти функции делают одно и тоже.

Для того, чтобы получить значение завершенного обещания, мы можем использовать блок .then():

hello().then((value) => console.log(value))

… или даже так:

hello().then(console.log)

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

Ключевое слово await

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

Вы можете использовать await при вызове любой функции, возвращающей обещание, включая функции Web API.

Вот тривиальный пример:

async function hello(){
    return greeting = await Promise.resolve('Hello')
}

hello().then(alert)

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

Переписываем код на обещаниях с использованием async/await

Возьмем пример с fetch из предыдущей статьи:

fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
    let objectURL = URL.createObjectURL(myBlob)
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
})
.catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message)
})

У вас уже должны быть понимание того, что такое обещания и как они работают, но давайте перепишем этот код с использованием async/await, чтобы увидеть насколько все стало проще:

async function myFetch(){
    let response = await fetch('coffee.jpg')
    let myBlob = await response.blob()

    let objectURL = URL.createObjectURL(myBlob)
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
}

myFetch().catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message)
})

Это делает код намного проще и более легким для понимания — никаких блоков .then()!

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

async function myFetch(){
    let response = await fetch('coffee.jpg')
    return await response.blob()
}

myFetch().then((blob) => {
    let objectURL = URL.createObjectURL(blob)
    let image = document.createElement('image')
    image.src = objectURL
    document.body.appendChild(image)
}).catch(e => console.log(e))

Вы можете переписать пример или запустить наше живое демо (см. также исходный код).

Но как это работает?

Мы обернули код внутри функции и добавили ключевое слово async перед ключевым словом function. Вам нужно создать асинхронную функцию, чтобы определить блок кода, в котором будет запускаться асинхронный код; await работает только внутри асинхронных функций.

Еще раз: await работает только в асинхронных функциях.

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

let response = await fetch('coffee.jpg')

Значение, возвращаемое обещанием fetch(), присваивается переменной response, когда данное значение становится доступным, и «парсер» останавливается на этой линии до завершения обещания. Как только значение становится доступным, парсер переходит к следующей строчке кода, которая создает Blob. Эта строчка также вызывает основанный на обещаниях асинхронный метод, поэтому здесь мы также используем await. Когда результат операции возвращается, мы возвращаем его из функции myFetch().

Это означает, что когда мы вызываем функцию myFetch(), она возвращает обещание, поэтому мы можем добавить к ней .then(), внутри которого мы обрабатываем отображение изображения на экране.

Вы, наверное, думаете «Вот здорово!», и вы правы — меньше блоков .then() для оборачивания кода, все это выглядит как синхронный код, поэтому он интуитивно понятен.

Добавляем обработку ошибок

Если вы хотите добавить обработку ошибок, у вас есть несколько вариантов.
Вы можете использовать синхронную структуру try...catch вместе с async/await. Этот пример является расширенной версией приведенного выше кода:

async function myFetch(){
    try{
        let response = await fetch('coffee.jpg')
        let myBlob = await response.blob()

        let objectURL = URL.createObjectURL(myBlob)
        let image = document.createElement('img')
        image.src = objectURL
        document.body.appendChild(image)
    } catch(e){
        console.log(e)
    }
}

myFetch()

Блок catch(){} принимает объект ошибки, который мы назвали «e»; теперь мы можем вывести его в консоль, это позволит нам получить сообщение о том, в каком месте кода произошла ошибка.

Если вы хотите использовать вторую версию кода, показанного выше, вам следует просто продолжать использовать гибридный подход и добавить блок .catch() в конец вызова .then(), следующим образом:

async function myFetch(){
    let response = await fecth('coffee.jpg')
    return await response.blob()
}

myFetch().then((blob) => {
    let objectURL = URL.createObjectURL
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
}).catch(e => console.log(e))

Это возможно, поскольку блок .catch() будет перехватывать ошибки, возникающие как в асинхронной функции, так и в цепочке обещаний. Если вы используете здесь блок try/catch, то не сможете обрабатывать ошибки, возникающие при вызове функции myFetch().

Вы можете найти оба примера на GitHub:
simple-fetch-async-await-try-catch.html (см. исходный код)
simple-fetch-async-await-promise-catch.html (см. исходный код)

Ожидание Promise.all()

Async/await основан на обещаниях, так что вы можете использовать все возможности последних. К ним, в частности, относится Promise.all() — вы легко можете добавить await к Promise.all(), чтобы записать все возвращаемые значения способом, похожим на синхронный код. Снова возьмем пример из предыдущей статьи. Держите вкладку с ним открытой, чтобы сравнить с показанным ниже кодом.

С async/await (см. живое демо и исходный код) он выглядит так:

async function fetchAndDecode(url, type){
    let repsonse = await fetch(url)

    let content

    if(type === 'blob'){
        content = await response.blob()
    } else if(type === 'text'){
        content = await response.text()
    }

    return content
}

async function displayContent(){
    let coffee = fetchAndDecode('coffee.jpg', 'blob')
    let tea = fetchAndDecode('tea.jpg', 'blob')
    let description = fetchAndDecode('description.txt', 'text')

    let values = await Promise.all([coffee, tea, description])

    let objectURL1 = URL.createObjectURL(values[0])
    let objectURL2 = URL.createObjectURL(values[1])
    let descText = values[2]

    let image1 = document.createElement('img')
    let image2 = document.createElement('img')
    image1.src = objectURL1
    image2.src = objectURL2
    document.body.appendChild(image1)
    document.body.appendChild(image2)

    let para = document.createElement('p')
    para.textContent = descText
    document.body.appendChild(para)
}

displayContent()
.catch(e => console.log(e))

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

let values = await Promise.all([coffee, tea, description])

Используя await, мы получаем результаты трех обещаний в переменной values, способом, похожим на синхронный код. Мы должны обернуть всю функцию в новую асинхронную функцию, displayContent(). Мы не достигли сильного сокращения кода, но получили возможность извлечь большую часть кода из блока .then(), что обеспечивает полезное упрощение и делает код более читаемым.

Для обработки ошибок мы добавили блок .catch() в наш вызов displayContent(); он обрабатывает ошибки обеих функций.

Запомните: также можно использовать блок .finally() для получения отчета о выполнении операции — вы можете увидеть это в действии в нашем живом демо (см. также исходный код).

Недостатки async/await

Async/await имеет парочку недостатков.

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

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

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

В нашем распоряжении имеется два примера — slow-async-await.html (см. исходный код) и fast-async-await.html (см. исходный код). Оба примера начинаются с функции-обещания, которая имитирует асинхронную операцию с помощью setTimeout():

function timeoutPromise(interval){
    return new Promise((resolve, reject) => {
        setTimeout(function(){
            resolve('done')
        }, interval)
    })
}

Затем следует асинхронная функция timeTest(), которая ожидает трех вызовов timeoutPromise():

async function timeTest(){
    ...
}

Каждый из трех вызовов timeTest() завершается записью времени выполнения обещания, затем записывается время выполнения всей операции:

let startTime = Date.now()
timeTest().then(() => {
    let finishTime = Date.now()
    let timeTaken = finishTime - startTime
    alert('Time taken in milliseconds: ' + timeTaken)
})

В каждом случае функция timeTest() отличается.

В slow-async-await.html timeTest() выглядит так:

async function timeTest(){
    await timeoutPromise(3000)
    await timeoutPromise(3000)
    await timeoutPromise(3000)
}

Здесь мы просто ожидаем три вызова timeoutPromise, каждый раз устанавливая задержку в 3 секунды. Каждый вызов ждет завершения предыдущего — если вы запустите первый пример, то увидите модальное окно примерно через 9 секунд.

В fast-async-await.html timeTest() выглядит так:

async function timeTest(){
    const timeoutPromise1 = timeoutPromise(3000)
    const timeoutPromise2 = timeoutPromise(3000)
    const timeoutPromise3 = timeoutPromise(3000)

    await timeoutPromise1
    await timeoutPromise2
    await timeoutPromise3
}

Здесь мы сохраняем три объекта Promise в переменных, что заставляет связанные с ним процессы выполняться одновременно.

Далее мы ожидаем их результаты — поскольку обещания начинают выполняться одновременно, обещания завершатся также в одно время; когда вы запустите второй пример, то увидите модальное окно примерно через 3 секунды!

Вам следует осторожно тестировать код и помнить об этом при снижении производительности.

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

Использование async/await совместно с классами

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

class Person{
    constructor(first, last, age, gender, interests){
        this.name = {
            first,
            last
        }
        this.age = age
        this.gender = gender
        this.interests = interests
    }

    async greeting(){
        return await Promise.resolve(`Hi! I'm ${this.name.first}`)
    }

    farewell(){
        console.log(`${this.name.first} has left the building. Bye for now!`)
    }
}

let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling'])

Метод класса может быть использован следующим образом:

han.greeting().then(console.log)

Поддержка браузеров

Одним из препятствий использования async/await является отсутствие поддержки старых браузеров. Эта возможность доступна почти во всех современных браузерах, также как обещания; некоторые проблемы существуют в Internet Explorer и Opera Mini.

Если вы хотите использовать async/await, но нуждаетесь в поддержке старых браузеров, можете использовать библиотеку BabelJS — она позволяет использовать новейший JS, преобразуя его в подходящий для конкретного браузера.

Заключение

Async/await позволяет писать асинхронный код, который легко читать и поддерживать. Несмотря на то, что async/await поддерживается хуже других способов написания асинхронного кода, его определенно стоит изучить.

Благодарю за внимание.

Счастливого кодинга!

Автор: aio350

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js