Руководство по Node.js, часть 7: асинхронное программирование

https://medium.freecodecamp.org/the-definitive-node-js-handbook-6912378afc6e
  • Перевод
Сегодня, в переводе седьмой части руководства по Node.js, мы поговорим об асинхронном программировании, рассмотрим такие вопросы, как использование коллбэков, промисов и конструкции async/await, обсудим работу с событиями.




Асинхронность в языках программирования


Сам по себе JavaScript — это синхронный однопоточный язык программирования. Это означает, что в коде нельзя создавать новые потоки, выполняющиеся параллельно. Однако компьютеры, по своей природы, асинхронны. То есть некие действия могут выполняться независимо от главного потока выполнения программы. В современных компьютерах каждой программе выделяется некое количество процессорного времени, когда это время истекает, система отдаёт ресурсы другой программе, тоже на некоторое время. Подобные переключения выполняются циклически, делается это настолько быстро, что человек попросту не может этого заметить, в результате мы думаем, что наши компьютеры выполняют множество программ одновременно. Но это иллюзия (если не говорить о многопроцессорных машинах).

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

Как правило, языки программирования являются асинхронными, некоторые из них дают программисту возможность управлять асинхронными механизмами, пользуясь либо встроенными средствами языка, либо специализированными библиотеками. Речь идёт о таких языках, как C, Java, C#, PHP, Go, Ruby, Swift, Python. Некоторые из них позволяют программировать в асинхронном стиле, используя потоки, запуская новые процессы.

Асинхронность в JavaScript


Как уже было сказано, JavaScript — однопоточный синхронный язык. Строки кода, написанного на JS, выполняются в том порядке, в котором они присутствуют в тексте, друг за другом. Например, вот вполне обычная программа на JS, демонстрирующая такое поведение:

const a = 1
const b = 2
const c = a * b
console.log(c)
doSomething()

Но JavaScript был создан для использования в браузерах. Его основной задачей, в самом начале, была организация обработки событий, связанных с деятельностью пользователя. Например — это такие события, как onClick, onMouseOver, onChange, onSubmit, и так далее. Как решать подобные задачи в рамках синхронной модели программирования?

Ответ кроется в окружении, в котором работает JavaScript. А именно, эффективно решать подобные задачи позволяет браузер, давая в распоряжение программиста соответствующие API.

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

Коллбэки


Если говорить о браузерном JavaScript, то можно отметить, что нельзя заранее узнать, когда пользователь щёлкнет по некоей кнопке. Для того чтобы обеспечить реакцию системы на подобное событие, для него создают обработчик.

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

document.getElementById('button').addEventListener('click', () => {
  //пользователь щёлкнул по элементу
})

Такие функции ещё называют функциями обратного вызова или коллбэками.

Коллбэк — это обычная функция, которая передаётся, как значение, другой функции. Вызвана она будет только в том случае, когда произойдёт некое событие. В JavaScript реализована концепция функций первого класса. Такие функции можно назначать переменным и передавать другим функциям (называемым функциями высшего порядка).

В клиентской JavaScript-разработке распространён подход, когда весь клиентский код оборачивают в прослушиватель события load объекта window, который вызывает переданный ему коллбэк после того, как страница будет готова к работе:

window.addEventListener('load', () => {
  //страница загружена
  //теперь с ней можно работать
})

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

setTimeout(() => {
  // выполнится через 2 секунды
}, 2000)

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

const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
  if (xhr.readyState === 4) {
    xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
  }
}
xhr.open('GET', 'https://yoursite.com')
xhr.send()

▍Обработка ошибок в коллбэках


Поговорим о том, как обрабатывать ошибки в коллбэках. Существует одна распространённая стратегия обработки подобных ошибок, которая применяется и в Node.js. Она заключается в том, что первым параметром любой функции обратного вызова делают объект ошибки. При отсутствии ошибок в этот параметр будет записано значение null. В противном случае тут будет объект ошибки, содержащий её описание и дополнительные сведения о ней. Вот как это выглядит:

fs.readFile('/file.json', (err, data) => {
  if (err !== null) {
    //обработаем ошибку
    console.log(err)
    return
  }
  //ошибок нет, обработаем данные
  console.log(data)
})

▍Проблема коллбэков


Коллбэками удобно пользоваться в простых ситуациях. Однако, каждый коллбэк — это дополнительный уровень вложенности кода. Если используется несколько вложенных коллбэков, это быстро приводит к значительному усложнению структуры кода:

window.addEventListener('load', () => {
  document.getElementById('button').addEventListener('click', () => {
    setTimeout(() => {
      items.forEach(item => {
        //код, делающий что-то полезное
      })
    }, 2000)
  })
})

В этом примере показано всего лишь 4 уровня кода, но на практике можно столкнуться и с большим количеством уровней, обычно это называют «адом коллбэков». Справиться с этой проблемой можно, используя другие языковые конструкции.

Промисы и async/await


Начиная со стандарта ES6 в JavaScript появляются новые возможности, которые облегчают написание асинхронного кода, позволяя обходиться без коллбэков. Речь идёт о промисах, которые появились в ES6, и о конструкции async/await, появившейся в ES8.

▍Промисы


Промисы (promise-объекты) — это один из способов работы с асинхронными программными конструкциями в JavaScript, который, в целом, позволяет сократить использование коллбэков.

Знакомство с промисами


Промисы обычно определяют как прокси-объекты для неких значений, появление которых ожидается в будущем. Промисы ещё называют «обещаниями» или «обещанными результатами». Хотя эта концепция существует уже многие годы, промисы были стандартизированы и добавлены в язык лишь в ES2015. В ES2017 появилась конструкция async/await, которая основана на промисах, и которую можно рассматривать в качестве их удобной замены. Поэтому, даже если не планируется пользоваться обычными промисами, понимание того, как они работают, важно для эффективного использования конструкции async/await.

Как работают промисы


После вызова промиса он переходит в состояние ожидания (pending). Это означает, что функция, вызвавшая промис, продолжает выполняться, при этом в промисе производятся некие вычисления, по завершении которых промис сообщает об этом. Если операция, которую выполняет промис, завершается успешно, то промис переводится в состояние «выполнено» (fulfilled). О таком промисе говорят, что он успешно разрешён. Если операция завершается с ошибкой, промис переводится в состояние «отклонено» (rejected).

Поговорим о работе с промисами.

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


API для работы с промисами даёт нам соответствующий конструктор, который вызывают командой вида new Promise(). Вот как создают промисы:

let done = true
const isItDoneYet = new Promise(
  (resolve, reject) => {
    if (done) {
      const workDone = 'Here is the thing I built'
      resolve(workDone)
    } else {
      const why = 'Still working on something else'
      reject(why)
    }
  }
)

Промис проверяет глобальную константу done, и, если её значение равно true, он успешно разрешается. В противном случае промис отклоняется. Используя параметры resolve и reject, являющиеся функциями, мы можем возвращать из промиса значения. В данном случае мы возвращаем строку, но тут может использоваться и объект.

Работа с промисами


Выше мы создали промис, теперь рассмотрим работу с ним. Выглядит это так:

const isItDoneYet = new Promise(
  //...

)
const checkIfItsDone = () => {
  isItDoneYet
    .then((ok) => {
      console.log(ok)
    })
    .catch((err) => {
      console.error(err)
    })
}

checkIfItsDone()

Вызов checkIfItsDone() приведёт к выполнению промиса isItDoneYet() и к организации ожидания его разрешения. Если промис разрешится успешно, сработает коллбэк, переданный методу .then(). Если возникнет ошибка, то есть промис будет отклонён, обработать её можно будет в функции, переданной методу .catch().

Объединение промисов в цепочки


Методы промисов возвращают промисы, что позволяет объединять их в цепочки. Удачным примером подобного поведения является браузерное API Fetch, представляющее собой уровень абстракции над XMLHttpRequest. Существует довольно популярный npm-пакет для Node.js, реализующий API Fetch, который мы рассмотрим позже. Это API можно использовать для загрузки неких сетевых ресурсов и, благодаря возможности объединения промисов в цепочки, для организации последующей обработки загруженных данных. Фактически, при обращении к API Fetch, выполняемом благодаря вызову функции fetch(), создаётся промис.

Рассмотрим следующий пример объединения промисов в цепочки:

const fetch = require('node-fetch')
const status = (response) => {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  }
  return Promise.reject(new Error(response.statusText))
}
const json = (response) => response.json()
fetch('https://jsonplaceholder.typicode.com/todos')
  .then(status)
  .then(json)
  .then((data) => { console.log('Request succeeded with JSON response', data) })
  .catch((error) => { console.log('Request failed', error) })

Здесь мы пользуемся npm-пакетом node-fetch и ресурсом jsonplaceholder.typicode.com в качестве источника JSON-данных.

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

  • status — числовое значение, представляющее собой код состояния HTTP.
  • statusText — текстовое описание кода состояния HTTP, которое представлено строкой OK в том случае, если запрос был выполнен успешно.

У объекта response есть метод json(), который возвращает промис, при разрешении которого выдаётся обработанное содержимое тела запроса, представленное в формате JSON.

Учитывая вышесказанное, опишем то, что происходит в этом коде. Первый промис в цепочке представлен объявленной нами функцией status(), которая проверяет состояние ответа, и, если он свидетельствует о том, что запрос не удался (то есть, код состояния HTTP не находится в диапазоне между 200 и 299), отклоняет промис. Эта операция приводит к тому, что другие выражения .then() в цепочке промисов не выполняются и мы сразу попадаем в метод .catch(), выводя в консоль, вместе с сообщением об ошибке, текст Request failed.

Если код состояния HTTP нас устраивает, вызывается объявленная нами функция json(). Так как предыдущий промис, при его успешном разрешении, возвращает объект response, мы используем его в качестве входного значения для второго промиса.

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

Обработка ошибок


В предыдущем примере у нас был метод .catch(), присоединённый к цепочке промисов. Если что-то в цепочке промисов идёт не так и возникает ошибка, либо если один из промисов оказывается отклонённым, управление передаётся в ближайшее выражение .catch(). Вот как выглядит ситуация, когда в промисе возникает ошибка:

new Promise((resolve, reject) => {
  throw new Error('Error')
})
  .catch((err) => { console.error(err) })

Вот пример срабатывания .catch() после отклонения промиса:

new Promise((resolve, reject) => {
  reject('Error')
})
  .catch((err) => { console.error(err) })

Каскадная обработка ошибок


Что делать, если в выражении .catch() возникнет ошибка? Для обработки такой ошибки можно включить в цепочку промисов ещё одно выражение .catch() (а потом можно присоединить к цепочке ещё столько выражений .catch(), сколько понадобится):

new Promise((resolve, reject) => {
  throw new Error('Error')
})
  .catch((err) => { throw new Error('Error') })
  .catch((err) => { console.error(err) })

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

Promise.all()


Если вам нужно выполнить некое действие после разрешения нескольких промисов, сделать это можно с помощью команды Promise.all(). Рассмотрим пример:

const f1 = fetch('https://jsonplaceholder.typicode.com/todos/1')
const f2 = fetch('https://jsonplaceholder.typicode.com/todos/2')
Promise.all([f1, f2]).then((res) => {
    console.log('Array of results', res)
})
.catch((err) => {
  console.error(err)
})

В ES2015 появился синтаксис деструктурирующего присваивания, с его использованием можно создавать конструкции следующего вида:

Promise.all([f1, f2]).then(([res1, res2]) => {
    console.log('Results', res1, res2)
})

Тут мы, в качестве примера, рассматривали API Fetch, но Promise.all(), конечно, позволяет работать с любыми промисами.

Promise.race()


Команда Promise.race() позволяет выполнить заданное действие после того, как будет разрешён один из переданных ей промисов. Соответствующий коллбэк, содержащий результаты этого первого промиса, вызывается лишь один раз. Рассмотрим пример:

const first = new Promise((resolve, reject) => {
    setTimeout(resolve, 500, 'first')
})
const second = new Promise((resolve, reject) => {
    setTimeout(resolve, 100, 'second')
})
Promise.race([first, second]).then((result) => {
  console.log(result) // second
})

Об ошибке Uncaught TypeError, которая встречается при работе с промисами


Если, работая с промисами, вы столкнётесь с ошибкой Uncaught TypeError: undefined is not a promise, проверьте, чтобы при создании промисов использовалась бы конструкция new Promise() а не просто Promise().

▍Конструкция async/await


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

Конструкция async/await позволяет уменьшить объём шаблонного кода, который приходится писать при работе с промисами. Когда промисы появились в стандарте ES2015, они были направлены на решение проблемы создания асинхронного кода. Они с этой задачей справились, но за два года, разделяющие выход стандартов ES2015 и ES2017, стало понятно, что считать их окончательным решением проблемы нельзя.

Одной из проблем, которую решали промисы, был знаменитый «ад коллбэков», но они, решая эту проблему, создали собственные проблемы схожего характера.

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

Как работает конструкция async/await


Асинхронная функция возвращает промис, как, например, в следующем примере:

const doSomethingAsync = () => {
    return new Promise((resolve) => {
        setTimeout(() => resolve('I did something'), 3000)
    })
}

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

const doSomething = async () => {
    console.log(await doSomethingAsync())
}

Объединим два вышеприведённых фрагмента кода и исследуем его поведение:

const doSomethingAsync = () => {
    return new Promise((resolve) => {
        setTimeout(() => resolve('I did something'), 3000)
    })
}
const doSomething = async () => {
    console.log(await doSomethingAsync())
}
console.log('Before')
doSomething()
console.log('After')

Этот код выведет следующее:

Before
After
I did something

Текст I did something попадёт в консоль с задержкой в 3 секунды.

О промисах и асинхронных функциях


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

const aFunction = async () => {
  return 'test'
}
aFunction().then(console.log) // Будет выведен текст 'test'

Эта конструкция аналогична такой:

const aFunction = async () => {
  return Promise.resolve('test')
}
aFunction().then(console.log) // Будет выведен текст 'test'

Сильные стороны async/await


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

const getFirstUserData = () => {
  return fetch('/users.json') // загрузить список пользователей
    .then(response => response.json()) // разобрать JSON
    .then(users => users[0]) // выбрать первого пользователя
    .then(user => fetch(`/users/${user.name}`)) // загрузить данные о пользователе
    .then(userResponse => userResponse.json())  // разобрать JSON
}
getFirstUserData()

Вот как выглядит решение той же задачи с использованием async/await:

const getFirstUserData = async () => {
  const response = await fetch('/users.json') // загрузить список пользователей
  const users = await response.json() // разобрать JSON
  const user = users[0] // выбрать первого пользователя
  const userResponse = await fetch(`/users/${user.name}`) // загрузить данные о пользователе
  const userData = await userResponse.json() // разобрать JSON
  return userData
}
getFirstUserData()

Использование последовательностей из асинхронных функций


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

const promiseToDoSomething = () => {
    return new Promise(resolve => {
        setTimeout(() => resolve('I did something'), 10000)
    })
}
const watchOverSomeoneDoingSomething = async () => {
    const something = await promiseToDoSomething()
    return something + ' and I watched'
}
const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
    const something = await watchOverSomeoneDoingSomething()
    return something + ' and I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => {
    console.log(res)
})

Этот код выведет следующий текст:

I did something and I watched and I watched as well

Упрощённая отладка


Промисы сложно отлаживать, так как при их использовании нельзя эффективно пользоваться обычными инструментами отладчика (наподобие «шага с обходом», step-over). Код же, написанный с использованием async/await, можно отлаживать с использованием тех же методов, что и обычный синхронный код.

Генерирование событий в Node.js


Если вы работали с JavaScript в браузере, то вы знаете, что события играют огромнейшую роль в обработке взаимодействий пользователей со страницами. Речь идёт об обработке событий, вызываемых щелчками и движениями мыши, нажатиями клавиш на клавиатуре и так далее. В Node.js можно работать с событиями, которые программист создаёт самостоятельно. Здесь можно создать собственную систему событий с использованием модуля events. В частности, этот модуль предлагает нам класс EventEmitter, возможности которого можно задействовать для организации работы с событиями. Прежде чем воспользоваться этим механизмом, его нужно подключить:

const EventEmitter = require('events').EventEmitter

При работе с ним нам доступны, кроме прочих, методы on() и emit(). Метод emit используется для вызова событий. Метод on используется для настройки коллбэков, обработчиков событий, которые вызываются при вызове определённого события.

Например, давайте создадим событие start. Когда оно происходит, будем выводить что-нибудь в консоль:

eventEmitter = new EventEmitter();

eventEmitter.on('start', () => {
  console.log('started')
})

Для того чтобы вызвать это событие, используется следующая конструкция:

eventEmitter.emit('start')

В результате выполнения этой команды вызывается обработчик события и строка started попадает в консоль.

Обработчику событий можно передавать аргументы, представляя их в виде дополнительных аргументов метода emit():

eventEmitter.on('start', (number) => {
  console.log(`started ${number}`)
})
eventEmitter.emit('start', 23)

Похожим образом поступают и в случаях, когда обработчику надо передать несколько аргументов:

eventEmitter.on('start', (start, end) => {
  console.log(`started from ${start} to ${end}`)
})
eventEmitter.emit('start', 1, 100)

Объекты класса EventEmitter имеют и некоторые другие полезные методы:

  • once() — позволяет зарегистрировать обработчик события, который можно вызвать лишь один раз.
  • removeListener() — позволяет удалить переданный ему обработчик из массива обработчиков переданного ему события.
  • removeAllListeners() — позволяет удалить все обработчики переданного ему события.

Итоги


Сегодня мы поговорили об асинхронном программировании на JavaScript, в частности, обсудили коллбэки, промисы и конструкцию async/await. Здесь же мы коснулись вопроса работы с событиями, описываемыми разработчиком средствами модуля events. Нашей следующей темой будут механизмы организации сетевого взаимодействия платформы Node.js.

Уважаемые читатели! Пользуетесь ли вы, при программировании для Node.js, конструкцией async/await?

RUVDS.com

805,00

RUVDS – хостинг VDS/VPS серверов

Поделиться публикацией
Комментарии 31
    0
    Уважаемые читатели! Пользуетесь ли вы, при программировании для Node.js, конструкцией async/await?
    Пользуемся конечно! :)

    появилась конструкция async/await. Её использование позволяет писать код, который выглядит как синхронный, но при этом является асинхронным, в частности, не блокирует главный поток.
    Вот эта фраза мне не до конца понятна. Главный поток не блокируется, но что это даёт однопоточному Node.js приложению на практике, какие ещё операции могут в это время выполняться?! Допустим у меня какой-нибудь Express.js backend:
    var usersRouter = require('./routes/users');
    
    app.use('/users', usersRouter);

    вызывает асинхронную функцию ./routes/users.js:
    var express = require('express');
    var router = express.Router();
    
    router.get('/', async function(req, res, next) {
      res.send('respond with a resource');
    });
    Во время выполнения этой функции асинхронно бэкэнду приходит другой запрос от другого пользователя — что произойдёт? Обработка запроса начнёт выполняться или всё равно придётся ждать завершения асинхронной функции?
      0
      В общем случае это зависит от того как написан код. Конкретно в случае express — другой запрос начнет выполняться одновременно с первым, да и было бы очень странно если бы это было не так.

      Кстати, async вы могли бы и не писать, потому что у вас нет ни одного вызова await.
        0
        В общем случае это зависит от того как написан код. Конкретно в случае express — другой запрос начнет выполняться одновременно с первым, да и было бы очень странно если бы это было не так.
        Вот этот момент ОЧЕНЬ важен! Где об этом можно почитать, удостовериться?

        Кстати, async вы могли бы и не писать, потому что у вас нет ни одного вызова await.
        await должен по идее вызвать Express. Если это не так, то как второй запрос начнёт исполняться одновременно с первым?
          0
          await должен по идее вызвать Express

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


          Где об этом можно почитать, удостовериться?

          Проще всего — взять да проверить на практике.

            0
            Проще всего — взять да проверить на практике.
            Т.е. вы точно не знаете. Зачем было писать тогда?!

            Кто-то может ответить на этот вопрос?
              0
              Я совершенно точно знаю ответ на вопрос «будут ли запросы выполняться параллельно». Но я не знаю ответа на вопрос «где об этом написано».
                0
                Спасибо! :)
        0
        Во первых нужно отличать код который будет вызыватся при иницализации модуля и код который будет вызван при событии запроса. Сначала у вас происходит регистрация колбеков а потом уже сервер может принимать запросы.
        И второй момент, важно понимать что весь код внутри колбека тоже синхронный, и существует такая вещь как очередь колбеков т.е пока не выполнится весь код внутри функции следующий колбек не сработает.
        0
        Кто-нибудь тут знает образом async/await работает при обработке сложных/долгих операций? Также, как и промисы?

        Я знаю, что промисы просто блокируют ядро до момента пока эта долгая операция не будет выполнена (как и обычные синхронные функции). Поэтому приходится перекладывать эту задачу на другие ядра процессора, используя воркеров, несмотря на то, что промисы считаюся «асинхронными»
          –2
          mayorovp, вот вам и практика.
            +1
            Все смешалось в кучу…

            async/await — это и есть сахар для промисов.

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

            Воркеры нужны не для любых операций, а только для ограниченных процессорным временем (CPU-bound), то есть вычислительных.

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

            Есть в ноде и синхронное API, но оно не рекомендуется к использованию кроме как при инициализации модуля.
              0
              Да, я имел ввиду поток, а не ядро.

              Жаль что ни в одной статье о промисах не объясняется про «операции, ограниченные процессорным временем». Создается ложное впечатление, что они позволяют выполнить абсолютно любую операцию на заднем фоне с «низким приоритетом» (с прерываниями), не блокируя при этом интерфейс и другие функции.
                0
                Колбэк промиса исполняется синхронно, поэтому блокирует поток.
                  0
                  такие вопросы отпадают сами собой когда разбираешься что такое event loop, как он работает. Спойлер: асинхронно выполняются только IO операции. Если вы или библиотеки которые вы используете выполняют какие то длинные вычисления то да, event loop на время выполнения таких функций будет их ждать.
                  Тяжелые вычисления можно выполнять только в отдельных потоках/процессах (если нужно не блокировать основной поток).
                    0
                    Тяжелые вычисления можно выполнять только в отдельных потоках/процессах

                    Насколько я понимаю долгие синхронные вычисления можно сделать «не блокирующими» добавив в эту функцию прерыватель setTimeout(50, func) и сохранять прогресс на каждой итерации. Это бы позволило event loop не ждать завершения этой функции. Конечно, это сложнее чем просто перенести выполнение в другой процесс, но все же это возможно
                      0
                      Это работает только если таких вычислений не слишком много. Лучше все-таки выносить их в другой поток (через воркеров).
                        0
                        Достаточно эту функцию обозначить как async, а внутри периодически вызывать:
                        await Promise.resolve(true);
                        Это позволит прерывать вычисления без дополнительного кода для сохранения результатов.
                          0
                          Хмм, не знал об этом способе, спасибо за информацию!
                            –1

                            true не нужен, достаточно await Promise.resolve().


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


                            await new Promise(resolve => setTimeout(resolve, 0));

                            или вот так:


                            await { then: next => setTimeout(next, 0) };
                          0
                          IO, по идеи, обрабатывается синхронно, но не в блокирующем режиме.
                    0
                    Как перебор с сахаром ведёт к быстрому ожирению, так и чрезмерное использование async/await может привести к замедлению приложения. Мне не раз приходилось наблюдать, когда вместо параллельного вызова нескольких функций, использующих промисы, люди использовали await и совершали вызовы последовательно, хотя этого не требовалось. Поэтому перед тем, как использовать await, всегда стоит крепко подумать, а точно ли там надо чего-то ждать, или можно использовать технику «fire and forget» и не плодить бесполезные ожидания.
                      +1
                      Почему бы не обернуть Promise.all и await-нуть полученный промис?
                        0
                        Можно, но зачем? Зачем ждать чего-то, чего ждать не совсем обязательно? По моему опыту, логику исполнения в абсолютном большинстве случаев можно построить так, чтобы избежать применения await в принципе. И это как раз вписывается в архитектуру как Node, так и просто web фронтенда гораздо гармоничнее, чем линейное исполнение кода.
                          +1
                          Можно, но зачем? Код с применением async/await становится более лаконичный для чтения, так же его проще дебажить, так как там нет бесконечных цепочек .then() в stack trace. И что вы понимаете под
                          Зачем ждать чего-то, чего ждать не совсем обязательно?

                          А разве обычно не строится концепт на ожидании результата? Если речь идёт про последовательные обещания, то решение досточно простое. Всё можно запустить паралельно, нужно запустить паралельно.

                          В любой технологии можно прострелить себе ногу, если не уметь грамоно ей распоряжаться.
                            0
                            Я строю концепты на реактивщине в основном и там, если результат получен, оповещаются наблюдатели, а не крутится что-то где-то в ожидании чего-то. Как-то удаётся обойтись без await в принципе. Поэтому всё работает бодро и без затыков.
                            0
                            Погодите, вы утверждаете, что ждать вообще не нужно? Я что-то не очень понимаю. Если вы делаете запрос к БД, например, то ждать вам придется, иначе вы не сможете ответить клиенту. Вот и выходит что-то вроде:
                            let result = await database.collection.find(query).exec();
                            res.json({result: result});

                            Не очень понимаю, как вы можете модифицировать этот код так, чтобы ждать было не нужно?

                            Если же говорить о случаях, когда совсем-совсем не надо ждать, в голову приходит что-то вроде удаления временного файла после выполнения запроса. Но это делается не то что без await, но и без промисов. Достаточно просто использовать пустой колбэк:
                            fs.unlink('tempfile.tmp', e => {});

                            Но это всё же достаточно редкий случай, чаще всего как раз ждать приходится.
                              –1
                              Как раз код с ожиданием результата из базы с последующей отправкой в res.json() просто заворачивается в промис. Да, коллбэк. Для того, чтобы спокойно относиться к коллбэкам, просто не нужно создавать из них ад :)
                              Если что, я в JS пришёл после 25+ лет системного программирования и для меня await — по умолчанию сигнал того, что здесь может быть место потери производительности и общая логика при любой возможности должна быть изменена на такую, которая не требует ожидания — неважно, ожидание ли системного семафора или позорное while (something) sleep(WAIT_DELAY);
                                0
                                Ну и при чем тут семафоры и активное ожидание, если за await скрывается тот же самый вызов then у промиса?
                                  0
                                  А речь и не шла изначально о том, что конкретно await в Node чем-то отличается от then колбэка. Речь идёт о построении логики работы приложения — стремление всё уложить в «синхронный» поток с помощью await чревато деградацией производительности из-за того, что увлёкшись этой самой линейностью, люда часто забывают о порядка запуска параллельных задач. Отказ от использования await в данном случае просто побуждает искать более выверенные пути data & event flow. Но да, определённой ценой читабельности кода.
                                    0

                                    А можно узнать где именно идёт деградация производительности? В основе Node.js до сих пор используются потоки, реализованные через Observer как и многие другие вещи. Мне кажется вы путаете упрощение с усложнением, так-как читать «реактивный код» на примере с Rx.js сложнее чем последовательные вызовы. Как я уже и писал выше, если программист отдаёт отчёт о том что он делает, такие банальные ошибки не будут допущены.

                                      0
                                      Выше уже упоминали использование Promise.all, о котором вспоминают только отдающие себе отчёт программисты, которых, по моим наблюдениям, мало. Rx.js мне пока не продали (может, просто мало пытались пока), мой собственный «реактивный код» читать не так сложно, я надеюсь — там простецкие on/off/once/emit и троттлинг по месту, вводить какие-то ещё сущности я не вижу необходимости, чтобы в конце концов не получилось как в том анекдоте «а сейчас мы со всей этой фигнёй попробуем взлететь».

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое