Рубрики
Без рубрики

Как сериализировать параллельные операции в JavaScript: обратные вызовы, обещания и Async / ждут

Как сериализировать параллелизм в JS с обратными вызовами, обещаниями и Async / ждут. Помечено JavaScript, обратные вызовы, обещания, ждут.

Обзор

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

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

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

Существуют в основном 3 способа сделать это в современном JavaScript.

  • Самый старый способ – использовать только обратные вызовы. Этот подход, возможно, концептуально самый чистый, но он также может привести к так называемому обратный ад : Вид кода спагетти, который может быть трудно понять и отлаживать.
  • Другой подход – использовать Обещания , который позволяет последовательности операций указывать более процедурным образом.
  • Совсем недавно JavaScript представил async и ждать Отказ

Эти подходы не являются взаимоисключающими, а скорее дополняют: Async/await Создание обещаний и обещает использовать обратные вызовы.

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

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

  • Установите ОС
  • Развернуть наше программное обеспечение
  • Пробежать тесты

Для любой заданной цели эти 3 операции должны работать в последовательности, но их можно выполнить одновременно по целям (благодаря EDA-QA для предложения этого практического примера!).

Одновременное исполнение

Сначала давайте посмотрим на какой-нибудь код, который управляет этими задачами одновременно, не сериализуя их вообще (Unserized.js):

const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min

const installOS = () => asyncTask("Install OS")

const deploySoftware = () => asyncTask("Deploy Software")

const runTests = () => asyncTask("Run Tests")

const taskDone = (name) => console.log(`Completed async "${name}"`)

const asyncTask = (name) =>  {
    console.log(`Started async "${name}"...`)
    setTimeout(() => taskDone(name), random(1,3) * 1000)
    console.log(`Returning from async "${name}"`)
}

const main = ()=> {
    installOS()
    deploySoftware()
    runTests()
}

main()

Мы смоделируем наши операции, позвонив Asynctask , который использует Setimeate ждать от 1 до 3 секунд до завершения его задачи и звонить TaskDone Отказ

Ниже приведен типичный вывод (фактический заказ изменится каждый раз, когда этот код запущен):

C:\dev\asyncio>node unserialized.js
Started async "Install OS"...
Returning from async "Install OS"
Started async "Deploy Software"...
Returning from async "Deploy Software"
Started async "Run Tests"...
Returning from async "Run Tests"
Completed async "Deploy Software"
Completed async "Install OS"
Completed async "Run Tests"

Как мы видим, это не так хорошо: мы развернули наше программное обеспечение до ОС даже была сделана установкой!

Используя обратные вызовы

Хорошо, давайте будем использовать обратные вызовы, чтобы исправить эту проблему (Callbacks.js):

const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min

const installOS = (nextTask) => asyncTask("Install OS", nextTask)

const deploySoftware = (nextTask) => asyncTask("Deploy Software", nextTask)

const runTests = () => asyncTask("Run Tests")

const taskDone = (name, nextTask) => {
    console.log(`Completed async "${name}"`)
    if (nextTask) {
        nextTask()
    }
}

const asyncTask = (name, nextTask) =>  {
    console.log(`Started async "${name}"...`)
    setTimeout(() => taskDone(name, nextTask), 
        random(1,3) * 1000)
    console.log(`Returning from async "${name}"`)
}

const main = ()=> {
    installOS(()=>deploySoftware(()=>runTests()))
}

main()

Мы называем installos с обратным вызовом, который будет работать deploysoftware. однажды installos сделано. Однажды Deploysoftware сделано, это позвонит свой собственный обратный вызов, сантесты функция.

Каждый раз, когда операция сделана, TaskDone Функция будет регистрировать операцию как завершенную и запустить следующую операцию.

Посмотрим, работает ли это:

C:\dev\asyncio>node callbacks.js
Started async "Install OS"...
Returning from async "Install OS"
Completed async "Install OS"
Started async "Deploy Software"...
Returning from async "Deploy Software"
Completed async "Deploy Software"
Started async "Run Tests"...
Returning from async "Run Tests"
Completed async "Run Tests"

Хорошо, мы видим, что каждый шаг происходит в порядке.

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

Обработка ошибок также, возможно, не так, как могло бы быть. Например, давайте изменим Deploysoftware Функция, чтобы бросить ошибку:

const deploySoftware = (nextTask) => {
    throw new Error('deploying software failed')
    asyncTask("Deploy Software", 
    nextTask)
}

И давайте попробуем наивно обернуть наш главный звонок с обработчиком исключений:

const main = ()=> {
    try {
        installOS(()=>deploySoftware(()=>runTests()))
    } catch (error) {
        console.log(`*** Error caught: '${error}' ***`)
    }
}

К сожалению, поймать Блок никогда не выполняется, и исключение заканчивается всплывающим стека:

C:\dev\asyncio\callbacks.js:7
        throw new Error('deploying software failed')
        ^

Error: deploying software failed
    at deploySoftware (C:\dev\asyncio\callbacks.js:7:8)
    at installOS (C:\dev\asyncio\callbacks.js:30:17)
    at taskDone (C:\dev\asyncio\callbacks.js:17:3)
    at Timeout.setTimeout [as _onTimeout] (C:\dev\asyncio\callbacks.js:23:19)
    at ontimeout (timers.js:458:11)
    at tryOnTimeout (timers.js:296:5)
    at Timer.listOnTimeout (timers.js:259:5)

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

Используя обещания

Давайте изменем наш код слегка, чтобы использовать обещания (Processies.js):

const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min

const installOS = () => asyncTask("Install OS")

const deploySoftware = () => asyncTask("Deploy Software")

const runTests = () => asyncTask("Run Tests")

const taskDone = (name) => console.log(`Completed async "${name}"`)

const asyncTask = (name) =>  {
    console.log(`Started async "${name}"...`)

    const promise = new Promise((resolve, reject) => {
        setTimeout(()=>resolve(name), random(1,3) * 1000)
    })

    console.log(`Returning from async "${name}"`)

    return promise
}

const main = ()=> {
    installOS().then(name=>{
        taskDone(name)
        return deploySoftware()
    }).then(name=>{
        taskDone(name)
        return runTests()
    }).then(taskDone)
}

main()

Мы видим, что мы смогли удалить NextTask Обратный вызов от наших задач. Теперь каждая задача может работать независимо. Работа связать их вместе была перенесена в Главная Отказ

Для этого мы модифицировали Asynctask вернуть обещание.

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

Это позволяет нам сериализовать наши асинхронные операции. Когда installos сделано, мы поставляем обратный вызов на тогда что звонит deploysoftware. . Deploysoftware Функция возвращает другое обещание, которое разрешается по телефону сантесты Отказ Когда сантесты Сделано, мы просто поставляем тривиальный обратный вызов, который просто регистрирует работу как сделано.

Возвращая объекты обещания из наших задач, мы можем объединить задачи, которые мы хотим завершить один за другим.

Детали того, как реализованы обещания, выходит за рамки этого поста. Если вы заинтересованы в этой теме, я нашел интересную Статья о дизайне Q Promise Библиотека Отказ

Я думаю, что этот код легче читать, чем пример обратного вызова.

Это также облегчает обрабатывать ошибки. Давайте снова изменим Deploysoftware Чтобы бросить ошибку:

const deploySoftware = () => {
    throw new Error('"Deploy Software" failed')
    return asyncTask("Deploy Software")
}

Обещания имеют удобный способ справиться с этим. Мы просто добавляем поймать Метод до конца нашей цепочки обещания:

const main = ()=> {
    installOS().then(name=>{
        taskDone(name)
        return deploySoftware()
    }).then(name=>{
        taskDone(name)
        return runTests()
    }).then(taskDone)
    .catch((error)=>console.log(`*** Error caught: '${error}' ***`))
}

Если ошибка возникает во время попытки разрешить обещание, это поймать Способ называется.

Давайте посмотрим, что произойдет, когда мы запустим этот код:

C:\dev\asyncio>node serialize_with_promises.js
Started async "Install OS"...
Returning from async "Install OS"
Completed async "Install OS"
*** Error caught: 'Error: "Deploy Software" failed' ***

Отлично, мы поймали нашу ошибку! Я думаю, что это выглядит намного простым, чем пример чистого обратного вызова.

Использование Async/a ждать

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

Хорошо, больше не ожидая – давайте изменим наш предыдущий пример, чтобы использовать Async/await (async_await.js)!

const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min

const installOS = () => asyncTask("Install OS") 

const deploySoftware = () => asyncTask("Deploy Software") 

const runTests = () => asyncTask("Run Tests") 

const taskDone = (name) => console.log(`Completed async "${name}"`)

const asyncTask = (name) =>  {
    console.log(`Started async "${name}"...`)

    const promise = new Promise((resolve, reject) => {
        setTimeout(()=>resolve(name), random(1,3) * 1000)
    })

    console.log(`Returning from async "${name}"`)

    return promise
}

const main = async ()=> {
    const installOSResult = await installOS()
    taskDone(installOSResult)

    const deploySoftwareResult = await deploySoftware()
    taskDone(deploySoftwareResult)

    const runTestsResult = await runTests()
    taskDone(runTestsResult)
}

main()

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

ждать Будет автоматически ждать, пока обещание вернутся функцией для решения самого. Это не блокировка, как и весь код, который мы смотрели на сегодня, поэтому другие вещи могут одновременно работать, пока выражение ожидается. Тем не менее, следующая строка кода после ждать не будет работать, пока обещание не будет решено. Любая функция, которая содержит ждать должен быть помечен как async. .

Давайте запустим этот код и посмотрите на результаты:

C:\dev\asyncio>async_await.js
Started async "Install OS"...
Returning from async "Install OS"
Completed async "Install OS"
Started async "Deploy Software"...
Returning from async "Deploy Software"
Completed async "Deploy Software"
Started async "Run Tests"...
Returning from async "Run Tests"
Completed async "Run Tests"

Отлично, это работает!

Мы снова можем сделать небольшие изменения, потому что Deploysoftware Чтобы бросить ошибку:

const deploySoftware = () => {
    throw new Error('"Deploy Software" failed')
    return asyncTask("Deploy Software")
}

Давайте посмотрим, как мы можем справиться с этим:

const main = async ()=> {
    try {
        const installOSResult = await installOS()
        taskDone(installOSResult)

        const deploySoftwareResult = await deploySoftware()
        taskDone(deploySoftwareResult)

        const runTestsResult = await runTests()
        taskDone(runTestsResult)
    } catch(error) {
        console.log(`*** Error caught: '${error}' ***`)     
    }
}

Это работает:

C:\dev\asyncio>node async_await.js
Started async "Install OS"...
Returning from async "Install OS"
Completed async "Install OS"
*** Error caught: 'Error: "Deploy Software" failed' ***

Как видно, Async/await позволяет использовать стандартный синхронный синтаксис для обработки любых ошибок, которые производятся нашим асинхронным кодом!

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

const timer = () => setInterval(()=>console.log('tick'), 500)

const main = async ()=> {
    const t = timer()

    const installOSResult = await installOS()
    taskDone(installOSResult)

    const deploySoftwareResult = await deploySoftware()
    taskDone(deploySoftwareResult)

    const runTestsResult = await runTests()
    taskDone(runTestsResult)

    clearInterval(t)
}

Вот результат:

C:\dev\asyncio>node async_await.js
Started async "Install OS"...
Returning from async "Install OS"
tick
Completed async "Install OS"
Started async "Deploy Software"...
Returning from async "Deploy Software"
tick
tick
tick
tick
tick
tick
Completed async "Deploy Software"
Started async "Run Tests"...
Returning from async "Run Tests"
tick
tick
Completed async "Run Tests"

Мы можем подтвердить, что таймер продолжает работать, пока мы ждать наши задачи. Здорово!

При использовании Ждите Я думаю, что полезно иметь в виду, что оно примерно эквивалентно получить обещание обратно от асинхронного звонка и позвонить в его тогда метод.

Некоторые чтены: вы должны использовать Ждите в функции, отмеченной async. . Это означает, что вы не можете Ждите что-то в верхнем уровне кода JavaScript. При написании кода верхнего уровня вы можете использовать тогда Синтаксис обещаний вместо или оберните код в функции саморегулирования, которые вы отмечаете как async.

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

Связанный:

  • Ленивая оценка в JavaScript с генераторами, картой, фильтром и уменьшением
  • Тщательное изучение JavaScript ждут
  • Итераторы идут! [ Symbol.itoratorator] и [Symbol.asyntciterator] в JavaScript
  • Асинхронные генераторы и трубопроводы в JavaScript

Оригинал: “https://dev.to/nestedsoftware/how-to-serialize-concurrent-operations-in-javascript-callbacks-promises-and-asyncawait-3ge3”