В этой статье, я хочу рассказать про свое решение проблемы с асинхронной функциональностью javascript, по средствам введения полностью асинхронной модели вычислений. Будет описана сама концепция, и дана ссылка на реализацию. Заинтересовавшихся прошу под кат.
Введение
Начнем с главного — синхронно или асинхронно? Когда имеется синхронный процесс вычисления и асинхронная функциональность без которой не обойтись, это проблема. Более того, когда асинхронность более выигрышна и вообще best practice то эту проблему надо решать.
Что уже есть для решения? Есть коллбэки, есть промайсы. Но промайсы — это модифицированные коллбеки и только упрощают проблему, но не решают её. Для действительного решения проблемы, необходимо сводить все к одной модели — или полностью синхронной или полностью асинхронной.
В последних стандартах, появились свои промайсы, а потом и async/await, что вкупе позволяет свести вычислительный процесс к полностью синхронной модели. И казалось бы, что проблема решена, но у меня есть ряд претензий к данному решению:
- реализация далека от "элегантности"
 - на данный момент медленно работает
 - плодит плохо читаемый код
 - плохо подходит для организации "массивной" параллельности (без кучи плохо читаемого кода)
 
Критикуешь — предлагай
Давайте забудем про async/await, забудем про промайсы, как это должно выглядеть? Удобно, единообразно, с минимум дополнительного кода? Что то вроде функции, только асинхронной:
- что бы определение было как у синхронной
 - что бы принцип работы был как у синхронной
 - что бы работала внутри синхронного вычислительного процесса
 
То есть, хорошо бы превратить синхронную функцию JS в асинхронную. Давайте составим список шагов для выполнения данной задачи.
- Первое что нужно сделать, для чего-то такого, это введение собственного стека вызовов, но не простого, а древовидного! Классический стек вызовов не годится для массивной параллельности.
 - Второе, это сделать так. что бы вершина 
любойветви всегда была в работе, что бы поток исполнения гарантированно мог на нее вернутся в случае ухода. - Третье, это отказ от синхронного возврата функции, и замена на асинхронную альтернативу, при которой можно дожидаться чего либо, прежде чем вернуть результат. То есть нужен возврат по команде, а не по безальтернативному порядку исполнения, неудержимо стремящимуся к завершению.
 - Четвертое, это отойти от синхронного вычислительного процесса, внутри синхронной функции, что бы синхронные вызовы данной функции, просто "прокачивали" асинхронный процесс.
 - Пятое и последнее, это пара функций оберток для асинхронного вызова/возврата, которые все что надо разложат куда надо. Пусть они называются 
callиback. 
Вперед в дебри!
Давайте попробуем это все изобразить. По условию определение асинхронной функции, должно быть ровно такое же, как и синхронной.
function A ()
{
}Начнем с последнего
Пусть call:
- имеет параметры: текущая вершина, имя функции для вызова, аргументы для этой функции
 - создает новую вершину, для данной ветви, оставляя ссылку на текущую
 - вызывает указанную функцию с указанными аргументами
 
Пусть back:
- имеет параметры: текущая вершина, результат
 - удаляет текущую вершину, для данной ветви, переходит по ссылке на предыдущую
 - вызывает функцию, указанную для предыдущей вершины, с результатом, возвращенным функцией текущей вершины.
 
Теперь введем дерево вызовов
Тут выполняется первых 2 пункта:
- пусть будет некая начальная вершина, корень дерева, и все вызовы начинаются от нее
 - пусть вершина передается в качестве аргумента вызываемой функции.
 - пусть вершина будет единственным параметром вызываемой функции, и все необходимое, в том числе текущие аргументы данной функции хранятся в ней.
 - при вызове, вершина данной ветви захватывается контекстом исполнения функции
 - при последующем вызове вершина становится узлом ветви, новая вершина захватывается контекстом новой вызванной функции
 - при асинхронной функциональности, например ajax, вершина сохраняется в замыкании
 - в общем и целом: текущий вызов -> дальнейший вызов | ожидание в замыкании | возврат
 
function A ( top )
{
}Синхронный возврат нам больше не друг
Пункт три, как уже было описано выше, специальный метод-обертка back, при вызове (команде), переходит на один узел назад по дереву вызовов. Таким образом и осуществляется асинхронный возврат. Тут же условимся что синхронный возврат (return) больше не учитывается, и асинхронный вызов продолжает существовать даже после синхронного возврата.
Еще не все, не все так просто
Из вышеописанного становится понятно что вызовов конкретной синхронной функции, превращенной в асинхронную, будет не один а несколько:
- первый вызов — собственно, классический вызов функции, смотрите описание 
call - второй и последующие — для возврата из асинхронного подвызова, смотрите описание 
back 
Нужен способ ловить, или же вычленять куски кода нужные при каждом конкретном вызове, и переносить поток исполнения именно туда. К тому же нужна возможность сохранять данные между подвызов-возврат, так как контекст исполнения пропадает после каждого синхронного возврата. Введем следующее:
- пусть в 
topбудет введено полеmark - пусть 
callставитьmarkравной# - пусть 
backставитmarkравной имени функции из текущей вершины (которая удаляется) - пусть 
switchосуществляет направление потока исполнения - пусть данные, которые должны быть доступны все время, до асинхронного возврата (
back), записываются прямо вtop.x = 1; 
function A ( top )
{
    switch ( top.mark )
    {
        case '#':
            break;
        case 'B':
            break;
        case 'C':
            break;
    }
}Собираем все в месте:
- пусть 
callставит переданные аргументы вtop.arg - пусть 
backставит переданный результат вtop.ret 
function A ( top )
{
    switch ( top.mark )
    {
        case '#':
            call(top, 'B', 'some_data');
            break;
        case 'B':
            call(top, 'C', top.ret + 1);
            break;
        case 'C':
            back(top, {x: top.ret});
            break;
    }
}Из примера выше видно, что получается обычная синхронная функция, только растянутая, и с возможностью дождаться асинхронного действия перед возвратом. Так же можно заметить, разбиение на шаги, и их последовательное исполнение. Учтем это, и то что используется дерево вызовов, а не стек, и добавим параллельности:
- пусть в 
topбудет введено полеsize - пусть 
callувеличиваетsizeтекущей вершины на единицу - пусть 
backуменьшаетsizeпредыдущей вершины на единицу (текущая уничтожается) 
function A ( top )
{
    switch ( top.mark )
    {
        case '#':
            top.buf = [];
            call(top, 'B', 'some_data1');
            call(top, 'B', 'some_data2');
            call(top, 'B', 'some_data3');
            break;
        case 'B':
            top.buf.push(top.ret);
            if ( !top.size )
            {
                call(top, 'C', top.buf);
            }
            break;
        case 'C':
            back(top, top.ret);
            break;
    }
}Получается возможность запуска массивной параллельной задачи, на любом последовательном шаге общего асинхронного процесса исполнения функции. Так же возможность дождаться завершения этой задачи и накопить результат. Давайте это улучшим, а именно учтем то, что не всегда нужен результат top.ret конкретной функции, и было бы неплохо иметь возможность параллельного запуска различных функций в одной задаче:
- пусть в 
topбудет введено новое полеgroup - пусть 
markбудет заменено наgroup['#name'] - пусть 
sizeбудет заменено наgroup['#size'] - пусть в 
callбудет введен новый параметр groupMark - пусть 
callувеличиваетtop.group[groupMark]текущей вершины на единицу - пусть 
backуменьшаетtop.group[groupMark]предыдущей вершины на единицу (текущая уничтожается) - пусть 
callиbackуправляют значением специальных именtop.group['#name']иtop.group['#size']содержащих имя и размер текущей группы 
function A ( top )
{
    switch ( top.group['#name'] )
    {
        case '#':
            top.buf = [];
            call(top, '#group1', 'B1', 'some_data1');
            call(top, '#group1', 'B2', 'some_data2');
            call(top, '#group1', 'B'3, 'some_data3');
            break;
        case '#group1':
            top.buf.push(top.ret);
            if ( !top.size )
            {
                call(top, '#group2', 'C', top.buf);
            }
            break;
        case '#group2':
            back(top, top.ret);
            break;
    }
}Добавим еще возможность дождаться окончания нескольких запущенных групп, и на этом все :)
- пусть 
top.group['##size']будет содержать сумму всехtop.group['#size'] - пусть будет введено 
top.group['##name']содержащая имя группы с которой была вызвана данная функция (имя группы возврата) 
function A ( top )
{
    switch ( top.group['#name'] )
    {
        case '#':
            top.listB = [];
            top.listC = [];
            call(top, '#group1', 'B1', 'some_data1');
            call(top, '#group1', 'B1', 'some_data1');
            call(top, '#group1', 'B2', 'some_data2');
            call(top, '#group2', 'С1', 'some_data1');
            call(top, '#group2', 'С1', 'some_data1');
            call(top, '#group2', 'С2', 'some_data2');
            break;
        case '#group1':
            if (top.ret) {top.listB.push(top.ret);}
            if ( !top.group['##size'] )
            {
                back(top, {B: top.listB, C: top.listC});
            }
            break;
        case '#group2':
            if (top.ret) {top.listC.push(top.ret);}
            if ( !top.group['##size'] )
            {
                back(top, {B: top.listB, C: top.listC});
            }
            break;
    }
}Итог
Выше описана концепция асинхронной функции, позволяющая ввести полностью асинхронную модель вычисления, и при этом:
- сохраняется простота и упорядоченность кода
 - гарантируется единообразность
 - скорость работы куда выше чем у async/await, на данный момент (зависит от реализации)
 - широкие возможности для организации параллельных вычислений
 
Мной разработана довольно удачная реализация данной концепции, с упором на параллельные вычисления. Ключевая особенность — поддержка потоков (WebWorker) и возможность асинхронного вызова функций, в каком бы потоке они не находились.
Комментарии (8)

Staltec
22.01.2018 16:47+7Промайс… Кхм… translate.google.ru/?hl=ru#en/ru/promise нажмите на кнопочку с динамиком и послушайте как на самом деле звучит это слово.

pred8or
22.01.2018 16:56+1После стартового «по средствам» был вознаграждён «промайсами». Даже если дальше написано что-то гениальное, я об этом не узнаю

token
22.01.2018 16:49+1И вот еще, вот про это слышали? caolan.github.io/async решает множество проблем как с читаемостью кода, пониманием, лапшой и т. д. позволяя запускать синхронные и асинхронные задачи в любых мыслимых комбинациях.

dmitry_luzanov
22.01.2018 18:14+1В последних стандартах, появились свои промайсы, а потом и async/await, что вкупе позволяет свести вычислительный процесс к полностью синхронной модели. И казалось бы, что проблема решена, но у меня есть ряд претензий к данному решению:
- реализация далека от «элегантности»
 - на данный момент медленно работает
 - плодит плохо читаемый код
 - плохо подходит для организации «массивной» параллельности (без кучи плохо читаемого кода)
 
Можете хоть один пункт аргументировать? Или тег «юмор» добавьте.
justboris
22.01.2018 20:32Вы не поверите, но при трансляции генераторов и асинхронных функций Babel генерирует примерно такой же код, как и у вас в статье:
regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return fetch(); case 2: result = _context.sent; _context.next = 5; return result.json(); case 5: data = _context.sent; return _context.abrupt("return", data); case 7: case "end": return _context.stop(); } } }, _callee, this);
Очень странно, что набор switch/case, которые исполняются в неопределенном порядке, кажется вам более читаемым, чем исходник с async/await.

norlin
22.01.2018 22:25+1Давайте забудем про async/await
Что то вроде функции, только асинхроннойА давайте не будем забывать про нативную фичу языка, которую сделали для работы с асинронностью и которая именно что реализует "функции, только асинхронные"?
          
 
token
Иными словами вы просто создали свою собственную реализацию стэка вызовов? И для чего?