Передразнивая одного человека, асинхронность это кооперативная многозадачность плюс цикл обработки событий.
Упрощенно, есть набор функций, умеющих приостанавливать и возобновлять своё выполнение - корутин.
Корутины обычно приостанавливают своё выполнение после запроса операции ввода/вывода, но могут и по другим поводам - например, просто ожидание, или ожидание завершения другой корутины, или ещё что.
Ядром асинхронной программы является паттерн "реактор" - рабочий цикл. В контексте клиентского JS это рабочий цикл браузера, в контексте ноды - что-то отдельное, я полагаю (с нодой не работал).
Цикл делает следующее:
- ожидает завершения одной из операций ввода/вывода или ожидания (любой)
- определяет, какая корутина ожидала эту операцию
- передаёт ей управление
- корутина делает своё дело, обрабатывая результат операции
- потом корутина либо завершается, либо планирует еще одну операцию. И цикл возобновляется.
Отсюда и вытекают все плюсы и минусы. С одной стороны, переключение между корутинами происходит в явно указанные моменты времени, так что меньше возни с синхронизацией.
С другой стороны, длинные вычисления так не распараллелишь - только ввод/вывод. Ну или в длинном вычислении время от времени делать паузу, но выигрыша все равно не будет. Так что это имеет смысл только для IO-bound программ.