В этой статье я расскажу о результате своей второй попытки борьбы с колбеками в JavaScript. Первая попытка была описана в предыдущей статье. В комментариях к ней мне подсказали некоторые идеи, которые были реализованы в новом проекте — nsynjs (next synjs).



TLDR: nsynjs — это JavaScript-движок, который умеет дожидаться исполнения колбеков и исполнять инструкции последовательно.

Это достигается тем, что nsynjs разбивает код исполняемой функции на отдельные операторы и выражения, оборачивает их во внутренние функции, и исполняет их по одной.

Nsynjs позволяет писать полностью последовательный код, наподобие такого:

var i=0;
while(i<5) {
    wait(1000); //  <<-- долгоживущая функция с колбеком внутри
    console.log(i, new Date());
    i++;
}

или такого

function getStats(userId) {
    return { // <<-- выражение, содержащее несколько функций с колбеками
            friends: dbQuery("select * from firends where user_id = "+userId).data,
            comments: dbQuery("select * from comments where user_id = "+userId).data,
            likes: dbQuery("select * from likes where user_id = "+userId).data,
    };
}

Nsynjs поддерживает большинство конструкций ECMAScript 2015, включая циклы, условные операторы, исключения, блоки try-catch, замыкания (правильнее было бы перевести как «контекстные переменные»), и т.п.

По-сравнению с Babel он:

  • все ещё легче (81кб без минимизации),
  • не имеет зависимостей,
  • не требует компиляции,
  • исполняется значительно быстрее,
  • позволяет запускать и останавливать долгоживущие потоки.

Для иллюстрации разберем небольшой пример веб-приложения, которое:

  1. Получает список файлов через ajax-запрос
  2. Для каждого файла из списка:
  3. Получает файл через ajax-запрос
  4. Пишет содержимое файла на страницу
  5. Ждет 1 сек

Синхронный псевдокод для этого приложения выглядел бы так (забегая вперёд, реальный код почти такой же):

var data = ajaxGetJson("data/index.json");
for(var i in data) {
    var el = ajaxGetJson("data/"+data[i]);
    progressDiv.append("<div>"+el+"</div>");
    wait(1000);
};

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

Функция-обёртка обычно должна сделать следующее:

  • принять указатель на состояние вызывающего потока в качестве параметра (например ctx)
  • вызвать обёртываемую функцию с колбеком
  • вернуть объект в качестве параметра оператора return, результат колбека присвоить какому-либо свойству этого объекта
  • в колбеке вызвать ctx.resume() (если колбеков несколько, то выбрать самый последний)
  • установить функцию-деструктор, которая будет вызвана в случае прерывания потока.

Для всех функций-обёрток свойство 'synjsHasCallback' должно быть установлено в true.

Создадим простейшую обёртку для setTimeout. Так как мы не получаем никакие данные из этой функции, то оператор return здесь не нужен. В итоге получится такой код:

var wait = function (ctx, ms) {
    setTimeout(function () {
        console.log('firing timeout');
        ctx.resume(); // <<-- продолжить исполнение вызывающего потока
    }, ms);
};
wait.synjsHasCallback = true; // <<-- указывает движку nsynjs, что эта функция-обёртка с колбеком внутри

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

Обёртка тогда получится такой:

var wait = function (ctx, ms) {
    var timeoutId = setTimeout(function () {
        console.log('firing timeout');
        ctx.resume();
    }, ms);
    ctx.setDestructor(function () { 
        console.log('clear timeout');
        clearTimeout(timeoutId);
    });
};
wait.synjsHasCallback = true;

Также нам понадобится обёртка над функцией getJSON библиотеки jQuery. В простейшем случае она будет иметь такой вид:

var ajaxGetJson = function (ctx,url) {
    var res = {};
    $.getJSON(url, function (data) {
        res.data = data;
        ctx.resume();
    });
    return res;
};
ajaxGetJson.synjsHasCallback = true;

Этот код будет работать только если getJSON успешно получила данные. При ошибке ctx.resume() вызван не будет, и вызывающий поток никогда не возобновится. Чтобы обработать ошибки, код необходимо модифицировать код так:

var ajaxGetJson = function (ctx,url) {
    var res = {};
    var ex;
    $.getJSON(url, function (data) {
        res.data = data; // <<-- в случае успеха, сохранить данные
    })
    .fail(function(e) {
        ex = e;	// <<-- в случае ошибки, сохранить её
    })
    .always(function() {
        ctx.resume(ex); // <<-- продолжить вызывающий поток в любом случае,
                        //         вызвать в нём исключение если была ошибка
    });
    return res;
};
ajaxGetJson.synjsHasCallback = true;

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

var ajaxGetJson = function (ctx,url) {
    var res = {};
    var ex;
    var ajax = $.getJSON(url, function (data) {
        res.data = data; // <<-- в случае успеха, сохранить данные
    })
    .fail(function(e) {
        ex = e;	// <<-- в случае ошибки, сохранить её
    })
    .always(function() {
        ctx.resume(ex); // <<-- продолжить вызывающий поток в любом случае,
                        //         вызвать в нём исключение если была ошибка
    });
    ctx.setDestructor(function () { 
        ajax.abort();
    });
    return res;
};

Когда обёртки готовы, мы можем написать саму логику приложения:

function process() {
    var log = $('#log');
    log.append("<div>Started...</div>");

	// внутри синхронного кода нам доступна переменная  synjsCtx, в которой
	// содержится указатель на контекст текущего потока
    var data = ajaxGetJson(synjsCtx, "data/index.json").data;
    log.append("<div>Length: "+data.length+"</div>");
    for(var i in data) {
        log.append("<div>"+i+", "+data[i]+"</div>");
        var el = ajaxGetJson(synjsCtx, "data/"+data[i]);
        log.append("<div>"+el.data.descr+","+"</div>");
        wait(synjsCtx,1000);
    }
    log.append('Done');
}

Так как функция ajaxGetJson может в некоторых случая выбрасывать исключение, то имеет смысл заключить ее в блок try-catch:

function process() {
    var log = $('#log');
    log.append("<div>Started...</div>");
    var data = ajaxGetJson(synjsCtx, "data/index.json").data;
    log.append("<div>Length: "+data.length+"</div>");
    for(var i in data) {
        log.append("<div>"+i+", "+data[i]+"</div>");
        try {
            var el = ajaxGetJson(synjsCtx, "data/"+data[i]);
            log.append("<div>"+el.data.descr+","+"</div>");
        }
        catch (ex) {
            log.append("<div>Error: "+ex.statusText+"</div>");
        }
        wait(synjsCtx,1000);
    }
    log.append('Done');
}

Последний шаг — это вызов нашей синхронной функции через движок nsynjs:

nsynjs.run(process,{},function () {
	console.log('process() done.');
});

nsynjs.run принимает следующие параметры:

var ctx = nsynjs.run(myFunct,obj, param1, param2 [, param3 etc], callback)

  • myFunct: указатель на функцию, которую требуется выполнить в синхронном режиме
  • obj: объект, который будет доступен через this в функции myFunct
  • param1, param2, etc – параметры для myFunct
  • callback: колбек, который будет вызван при завершении myFunct.

Возвращаемое значение: Контекст состояния потока.

Под капотом


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

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

  • синхронные
  • обёртки над колбеками
  • нативные.

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

  • если указатель на функцию имеет свойство synjsBin, то функция будет исполнена через nsynjs в синхронном режиме
  • если указатель на функцию имеет свойство synjsHasCallback, то это функция-обёртка, поэтому nsynjs остановит на ней выполнение. Функция-обёртка должна сама в позаботиться о возобновлении вызывающего синхронного потока путем вызова ctx.resume() в колбаке.
  • Все остальные функции считаются нативными, и возвращающими результат немедленно.

Производительность


При парсинге nsynjs-движок пытается анализировать и оптимизировать элементы кода исходной функции. Например, рассмотрим цикл:

for(i=0; i<arr.length; i++) {
    res += arr[i];
}

Этот цикл будет оптимизирован и скомпилирован в одну внутреннюю функцию, которая будет выполняться почти также быстро, как и нативный код:

this.execute = function(state) {
    for(state.localVars.i=0; state.localVars.i<arr.length; state.localVars.i++) {
        state.localVars.res += state.localVars.arr[state.localVars.i];
    }
}

Однако, если в элементе кода встречаются вызовы функций, а также операторы continue, break и return, то оптимизация для них, а также для всех родительских элементов не выполнится.

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

Например оператор:

var n = Math.E

будет оптимизирован в одну функцию:

this.execute = function(state,prev, v) {
    return state.localVars.n = Math.E
}

Если же в операторе имеется вызов функции, то nsynjs не может знать тип вызываемой функции заранее:

var n = Math.random()

Поэтому весь оператор будет выполнен по-шагам:


this.execute = function(state) {
    return Math
}
..
this.execute = function(state,prev) {
    return prev.random
}
..
this.execute = function(state,prev) {
    return prev()
}
..
this.execute = function(state,prev, v) {
    return state.localVars.n = v
}

Ссылки


> Репозиторий на GitHub
> Примеры
> Тесты
> NPM
Поделиться с друзьями
-->

Комментарии (107)


  1. mwizard
    24.05.2017 00:06
    +10

    Стоило ли делать все это ради неявного async/await с переусложненным интерфейсом? В современном javascript не используются коллбэки, насколько мне известно — их место заняли Promise, которые, в свою очередь, пишутся в псевдосинхронном стиле через await.


    1. amaksr
      24.05.2017 00:30
      -1

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


      1. mwizard
        24.05.2017 00:31
        +6

        все еще не доступен на многих браузерах, особенно в крупных компаниях.

        Казалось бы, именно ради этого и был написан babel...


      1. bano-notit
        24.05.2017 00:47
        +7

        А этот движок прям сразу в крупных компаниях возьмут на вооружение...


      1. hubhito
        24.05.2017 22:03

        Есть такая штука как co https://github.com/tj/co
        позволяет в том числе подготовить код к переходу на async await. кроме того поддерживает thunk, т.е. можно работать со старым кодом на колбэках как если бы это были промисы


        1. hubhito
          24.05.2017 22:03

          ну и к нему нужен Babel, да, чтобы использовать генераторы


          1. norlin
            25.05.2017 12:48

            В ноде – не нужен. Но в ноде и async/await уже можно (начиная с 7.6)...


    1. amaksr
      01.06.2017 06:35
      -1

      В процессе более активного использования nsynjs список отличий от async/await сформировался такой:

      — в nsynjs нет надобности использовать ключевые слова async/await в коде, так как тип исполняемой функции проверяется в рантайме
      — в nsynjs отпадает надобность в промисах в принципе (хотя для поклонников можно добавить несколько строчек в код nsynjs чтобы проверять возвращаемый функцией результат в рантайме на предмет не промис ли он, и не надо ли подождать).
      — в nsynjs для исключительных ситуаций достаточно механизма try/catch/throw. Механизм Promise/then/catch/reject не нужен.

      Преимущества по-сравнению с async/await:

      — возможность запускать псевдо-потоки,
      — возможнось останавливать псевдо-потоки как изнутри, так и извне,
      — возможнось подчистить активные функции с колбеками при остановке потока извне (например на активный setTimeout автоматически вызвать clearTimeout),
      — возможность создавать конструкторы с асинхронными функциями внутри.


      1. mayorovp
        01.06.2017 06:40

        Что такое "возможность запускать псевдо-потоки" и почему это преимущество перед async/await?


      1. mwizard
        01.06.2017 06:46
        +1

        — в nsynjs нет надобности использовать ключевые слова async/await в коде, так как тип исполняемой функции проверяется в рантайме

        и это плохо. Javascript неспроста движется в сторону проверки всего и вся при компиляции, включая статическую типизацию.


        — в nsynjs отпадает надобность в промисах в принципе (хотя для поклонников можно добавить несколько строчек в код nsynjs чтобы проверять возвращаемый функцией результат в рантайме на предмет не промис ли он, и не надо ли подождать).

        прелесть промисов в явном, детерминированном управлении event loop — см. концепции greenlets, fibers и т.д. В nsynjs этого нет, и по дизайну невозможно.


        — в nsynjs для исключительных ситуаций достаточно механизма try/catch/throw. Механизм Promise/then/catch/reject не нужен.

        Наверное, я неправильно использовал промисы все это время?


        async function foo() {
            try {
                await bar();
            } catch(e) {
                // ...
            } finally {
                // ...
            }
        }

        — возможность запускать псевдо-потоки,

        В чем отличие от запущенной async-функции?


        — возможнось останавливать псевдо-потоки как изнутри, так и извне,

        В чем отличие от bluebird, который умеет отменять исполняющиеся промисы?


        — возможнось подчистить активные функции с колбеками при остановке потока извне (например на активный setTimeout автоматически вызвать clearTimeout),

        В чем отличие от обертки try/finally внутри тела async-функции, с clearTimeout внутри finally?


        — возможность создавать конструкторы с асинхронными функциями внутри.

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


        1. amaksr
          01.06.2017 07:57

          Javascript неспроста движется в сторону проверки всего и вся при компиляции

          В JS изначально даже var не было, да и сейчас практически все резольвятся в рантайме, поэтому я бы не стал брать JS в качестве примера как надо делать типизацию.

          прелесть промисов в явном, детерминированном управлении event loop — см. концепции greenlets, fibers и т.д. В nsynjs этого нет, и по дизайну невозможно.
          в nsynjs свой event loop, а также свои структуры с программными счетчиками, стеками, локальными переменными, closures и т.п.

          Наверное, я неправильно использовал промисы все это время?

          Имеется ввтиду внутри промисифицированных функций, или если промис вернули куда-то в не async-функцию

          В чем отличие от запущенной async-функции?
          в том что есть указатель на ее состояние, с её собственным event-loop-ом, по которому над ней можно иметь полный контроль.

          В чем отличие от bluebird, который умеет отменять исполняющиеся промисы?

          В блюберд придется писать код чтобы отслеживать активные промисы. Хотя это и не сложно. В нативных промисах этого нет, и, говорят, не будет.

          В чем отличие от обертки try/finally внутри тела async-функции, с clearTimeout внутри finally?
          Это придется делать везде, где вызывается промис с setTimeout? Либо делать async-обертку к промису, ну и отслеживать активные обертки. В nsynjs это делается автоматически, т.к. есть свой стек, по которому можно всегда узнать что сейчас активно.

          А за асинхронные конструкторы, как по уму, надо бы отрывать руки по самые ягодицы.
          Не буду холиварить. Видел их много раз, и не сказал бы, что изза них какие-то существенные проблемы. Это как goto, кому-то нравится, кому-то нет…


          1. mwizard
            01.06.2017 08:22

            В JS изначально даже var не было, да и сейчас практически все резольвятся в рантайме, поэтому я бы не стал брать JS в качестве примера как надо делать типизацию.

            В JS изначально много чего не было. Тот JS, который имеем сейчас, и тот, с которого начиналось, это два разных языка, с разными ценностями и идиомами. И современный JS семимильными шагами идет к сильной типизации с выведением типов.


            в nsynjs свой event loop, а также свои структуры с программными счетчиками, стеками, локальными переменными, closures и т.п.

            И все это, конечно же, неявное. И, кстати, зачем это все нужно, ведь рантайм уже все предоставляет?


            Имеется ввтиду внутри промисифицированных функций, или если промис вернули куда-то в не async-функцию

            Если вернули не в async-функцию, она с ним работать все равно не сможет, поэтому и исключение ловить не надо, т.к. исключение все равно не будет выброшено в контексте этой функции.


            в том что есть указатель на ее состояние, с её собственным event-loop-ом, по которому над ней можно иметь полный контроль.

            Насколько мне известно, запустить несколько event loop в одном потоке нельзя по определению этого самого event loop, т.к. каждый event loop должен выполнять ожидание событий на своем списке дескрипторов средствами операционной системы. Значит, каждая функция работает в отдельном потоке? Великолепно! И в таком случае, в чем отличие от WebWorkers? И как у вас решился вопрос отсутствия в JS любых механизмов многопоточной синхронизации?


            В блюберд придется писать код чтобы отслеживать активные промисы. Хотя это и не сложно. В нативных промисах этого нет, и, говорят, не будет.

            Имеющийся proposal был отозван из-за излишней сложности реализации в v8, насколько мне известно. придумают способ проще — будет.


            Это придется делать везде, где вызывается промис с setTimeout? Либо делать async-обертку к промису, ну и отслеживать активные обертки. В nsynjs это делается автоматически, т.к. есть свой стек, по которому можно всегда узнать что сейчас активно.

            "Explicit is better than implicit. Simple is better than complex." Я не понимаю, в чем проблема явно освободить занятый ресурс (да, таймер это ресурс), как не вижу проблемы в том, чтобы закрыть за собой сокет или файл. Что делать, если я хочу из nsynjs передать объект таймера за пределы вашей RAII-процедуры? Мне его убьет при выходе из процедуры, несмотря на то, что референс утек? Если не убьет, тогда в чем смысл? Если убьет, то как этим пользоваться?


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


            Мне бы хотелось больше узнать о мотивации, зачем это было сделано, и какие именно задачи это призвано решать, потому что, как мне кажется, очевидно, что с async/await это решение не конкурент.


            1. vintage
              01.06.2017 09:40
              -2

              Насколько мне известно, запустить несколько event loop в одном потоке нельзя по определению этого самого event loop, т.к. каждый event loop должен выполнять ожидание событий на своем списке дескрипторов средствами операционной системы.

              Они могут вкладываться друг в друга. Один event-loop может быть задачей в рамках другого event-loop. Очевидно, тут именно такой случай.


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

              Erlang, D


            1. amaksr
              01.06.2017 16:50

              Event loop в nsynjs это просто цикл while(...) {...}, который выполняется в главном jS-потоке. Этот цикл выполняет свои тики пока не встретит вызов функции. Указатель на функцию анализируется на предмет типа функции и надо ли подождать, и если надо, то просто цикл останавлиавается по break. Это похоже на невытесняющую многозадачность, но все происходит внутри одного процесса JS. Поэтому вебворкеры здесь вообще ни при чем.

              зачем в принципе нужно «отслеживать активные обертки»
              в случае, если псевдопоток nsynjs завершается извне (типа как по SIGHUP), и надо освободить ресурсы, инача они вызовут колбеки. Стандартный JS такие возможности не предоставляет, ну тоесть надо писать учет активных функций самому. Но раз уж у нас есть свой event-loop и свои стеки, то почему бы не реализовать чтобы это делалось автоматически?


        1. vintage
          01.06.2017 09:33
          -2

          прелесть промисов в явном, детерминированном управлении event loop — см. концепции greenlets, fibers и т.д.

          Нет, эти концепции не требуют никакой "явности". Вообще, странно слышать термин "явность" от человека, использующего async, который неявно превращает функцию в конечный автомат.


  1. Hazrat
    24.05.2017 00:13
    +4

    Столько сил потрачено зря, действительно async/await чем не синхронность?


    1. arvitaly
      24.05.2017 00:50

      Тем, что не позволяет скрывать реализацию функции, вот была у нас функция синхронной мы писали без await, стала синхронной мы должны везде изменить на await, в итоге процентов 20 высокоуровневого кода становится заполнено ключевым словом «await».

      По проекту, задумка интересна, но ИМХО, это должно решаться автоматически, т.е. компилятор видит, что функция асинхронна и всегда «подставляет» await. Если же хотим получить Promise из нее и выполнить асинхронно, пишем специальную функцию (по опыту, таких ситуаций 0.1%).


      1. Hazrat
        24.05.2017 01:10

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


      1. amaksr
        24.05.2017 01:32

        это должно решаться автоматически, т.е. компилятор видит, что функция асинхронна и всегда «подставляет» awai

        Согласен, именно так и должно быть. Вместо этого разработчики языка всем парят, что генераторы/промисы/async/await это круто и теперь надо всем учится программировать по-новому: массивы перебирать рекурсией, желательно хвостовой, и вообще переходить на ФП. Приходится за них это делать то, что должно было быть сделано много лет назад.


        1. raveclassic
          24.05.2017 02:19

          Недавно тут штудировал esdiscuss на тему do notation, которая позволила бы решить проблемы с синтаксисом при использовании монад, которые, в свою очередь, прекрасно справляются с описанной вами проблемой. Ну что уж, выводы неутешительные, конечно, хотя там сам Брендан Айк написал год назад «We'll get it»!


      1. mayorovp
        24.05.2017 14:02
        -1

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


        1. vintage
          24.05.2017 14:39

          Возможно. Нужно уметь замораживать и размораживать текущий стек вызовов. Делать это можно либо через node-fibers, либо через бросание исключения + реактивное программирование.


          1. mayorovp
            24.05.2017 17:30
            -1

            Как вы себе представляете замораживание стека вызовов компилятором?


            1. vintage
              24.05.2017 17:39
              -1

              Компилятором — никак. Это в рантайме делается.


              1. mayorovp
                24.05.2017 17:40
                -1

                А теперь прочитайте комментарий, на который я отвечал.


              1. arvitaly
                25.05.2017 05:00

                vintage
                Ну я не соглашусь, менять runtime javascript выйдет дороже, поэтому реально выстрелил typescript, babel и flow, а не куча компиляторов с тяжеленным или не кроссплатформенным рантаймом.
                mayorovp
                Компилятор вполне способен выводить типы, определять нужные ветви кода из чужого модуля (tree shaking), ну и никто не запрещает ввести ограничения на такой язык (которые так и так будут полезны).
                Другое дело, как это сделать красиво и явно для разработчиков, тут нужно менять и технологии, и культуру.


                1. vintage
                  25.05.2017 09:05

                  Что пиарят — то и выстреливает. node-fibers — вполне себе лёгкий рантайм. Лучше бы его стандартизировали, а не async/await. $mol_atom — кроссплатформенная либа кило на 10, но она больше про реактивное программирование, а синхронный код — приятный бонус.


      1. LestaD
        31.05.2017 20:06
        +2

        В вашем комментарии скрыта огромная проблема — неопытность в проектировании больших проектов. Ни один человек в здравом уме не будет менять функцию/метод настолько, чтобы потребовалась асинхронщина.
        В данном случае правильнее будет написать новую асинхронную фукцию, чтобы как раз, не ломать совместимость с предыдущим кодом. Введение этого «движка» в проект только ухудшит поддержку кода, по нескольким причинам:
        — Новый слой абстракции. Это всегда проблема, когда этот слой не нужен.
        — Неизвестная технология. Как новичкам, так и старичкам в проекте придется разбираться в работе этого модуля, придется исследовать баги, ждать пока Вы их поправите, или же править их самому.
        — Не стандарт. Этого нет в ECMAScript


  1. comerc
    24.05.2017 01:18
    +2

    Ключевой вопрос "Зачем?" — не нашел ответа.


    1. raveclassic
      24.05.2017 01:26

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


  1. norlin
    24.05.2017 07:50
    +1

    Автор, а вы видели модуль co, например?


    co(function*(){
        var data = yield ajaxGetJson("data/index.json");
        for(var i in data) {
            var el = yield ajaxGetJson("data/"+data[i]);
            progressDiv.append("<div>"+el+"</div>");
            yield wait(1000);
        };
    });


    1. amaksr
      24.05.2017 08:44

      Да, недостатки те же, что и у async/await:
      1. Если какая-то функция внизу стека стала async-await (или yield), то все вызывающие функции, и весь граф вызовов, надо также менять на async-await. Я считаю это неправильно, когда программист должен отвлекаться на такие вещи.
      2. Несовместимо с некоторыми браузерами (только через babel)


      1. norlin
        24.05.2017 09:13
        +1

        Кстати, спасибо: благодаря комментариям я внезапно узнал, что node.js уже нативно поддерживает async/await и отказался от co. Смысла городить велосипеды я не вижу, честно. В вашем случае всяких обёрток и прочей лишней работы приходится делать больше, чем в случае с async/await.


  1. vintage
    24.05.2017 09:12
    +3

    Не хотите ли добавить своё решение в коллекцию? https://github.com/nin-jin/async-js/


    1. amaksr
      24.05.2017 09:47

      Думаю теперь можно, пришлю на днях.


    1. amaksr
      26.05.2017 08:12

      Вот такое решение получилось:

      index.js
      var nsynjs = require('../../nsynjs');
      
      function synchronousApp(modules) {
      	var user = require.main.require( './user' );
      	var greeter = require.main.require( './greeter' );
      
          try {
              console.time('time');
              greeter.say('Hello', user);
              greeter.say('Bye', user);
              console.timeEnd('time');
          }
          catch(e) {
              console.log('error',e);
          }
      }
      
      nsynjs.run(synchronousApp,null,function () {
      		console.log('done');
      });
      


      1. vintage
        26.05.2017 08:42

        require('../../nsynjs');

        Может опубликуете в NPM?


        require.main.require( './user' );

        А почему не просто require( './user' )?


        exports.readFile = function (ctx,name) {

        Может сделать это универсальным враппером идущим с самой библиотекой?


        const readFileSync = synjs.fromAsync( fs.readFile )


        1. amaksr
          26.05.2017 09:02

          Может опубликуете в NPM?

          В NPM он есть: npm install nsynjs, и можно require('nsynjs');

          require.main.require( './user' )

          require, как оказалось, работает немного не так как хотелось бы с относительными путями: путь берётся относительно файла, из которого вызвана require. В случае nsynjs это значит будет относительно местоположения nsynjs.js, Пока непридумал как это победить, но наткнулся на require.main.require, которое позволяет искать относительно от начального файла приложения. require.main.require нужно только для синхронного кода.

          Может сделать это универсальным враппером идущим с самой библиотекой?
          Да, наверное можно врапперы нескольких самых общеупотребительных функций в нее включить


          1. vintage
            26.05.2017 10:35
            +1

            Резолвить относительно мейна — тоже не вариант. Я так понимаю вы парсите переданную функцию, транспилируете код и эвалите в контексте своего модуля? В этот момент можно подменять require на module.require того модуля, откуда взят код. Передать модуль можно, например, так: nsynjs.run(module,synchronousApp)


            1. amaksr
              26.05.2017 19:27

              Да, модули можно передавать в параметрах в синхронный код. Это так и сделано, например в файле user.js:

              ...
              var wrappers = require('./wrappers');
              nsynjs.run(synchronousCode,{},wrappers,function (m) {
                  module.exports = m;
              });

              Можно и так, и так, кому как больше нравится.
              В нативном require в node модули можно тоже грузить несколькими способами.


              1. vintage
                26.05.2017 20:20
                +1

                Нужно всё же такое решение, которое позволит по минимуму отступать от обычного кода.


  1. mayorovp
    24.05.2017 10:57
    -2

    По-сравнению с Babel он:
    • исполняется значительно быстрее,

    Тем, кто просто ищет решение, способное выполняться быстрее чем Babel, могу порекомендовать попробовать tsc (Typescript Compiler).


    1. amaksr
      24.05.2017 17:18

      Typescript — отличная вещь для отлавливания багов, связанных с динамической природой типов данных в JS, Но проблему колбеков он никак не решает.


      1. mayorovp
        24.05.2017 17:28
        -1

        При чем тут колбеки?


  1. bo883
    24.05.2017 11:39
    +4

    а мне нравится, интересное решения несмотря на то что пишут выше.


  1. justboris
    24.05.2017 12:17
    +2

    Ставить в достоинства маленький размер по сравнению с babel некорректно, потому что babel никто в браузер не грузит, а прогоняет код на этапе сборки.


  1. justboris
    24.05.2017 12:19
    +2

    А еще, я попробовал поставить breakpoint в как-бы-синхронной функции, а он не сработал.


    Как отлаживать такой код?


    1. amaksr
      24.05.2017 17:31

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


      1. raveclassic
        24.05.2017 18:04
        +3

        Вы уж простите, но отсутствие отладки — это шоустоппер.


      1. justboris
        24.05.2017 19:04
        +2

        А как вы планируете его делать?


        По факту вы исполняете не исходную функцию, а копию ее текста.


        Из-за этого не только дебаггера не получится, но еще и переменные из замыкания теряются


        function withParam(param) {
           return function() {
             console.log(param);
           }
        }
        
        nsynjs.run(withParam('test'), function (r) {
            console.log('Done');
        });
        

        Получаю ReferenceError: param is not defined


        1. amaksr
          24.05.2017 20:03
          -3

          Надо перенести и декларацию, и использование функции внутрь синхронного кода:

          	function synchronousCode() {
          		function withParam(param) {
          		   return function() {
          			 console.log(param);
          		   }
          		};
          
          		withParam('test')();
          	}
          
          	nsynjs.run(synchronousCode,{}, function (r) {
          		console.log('Done');
          	});
          
          

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


          1. raveclassic
            24.05.2017 20:22
            +5

            Ну вот ваше решение и начинает превращаться в тыкву. «Решив» одну проблему, вы принесли ворох новых — появились какие-то сомнительные ограничения.


        1. amaksr
          24.05.2017 21:22
          -1

          А как вы планируете его делать?

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


          1. raveclassic
            25.05.2017 00:43
            +3

            Дебаггер можно отображать на динамическом div-е. Для этого нужно для каждого элемента языка в прототипы добавить функцию
            Вы бы себя слышали :)

            Ох… Ну а если я под Node?


            1. amaksr
              25.05.2017 03:13
              -3

              Вы бы себя слышали :)

              А что я такого сказал? Структура с данными есть, контроль над исполнением тоже есть, причем полный. Исполняем шаг — рендерим структуру. В чем проблема?

              Ох… Ну а если я под Node?
              Под нодой можно поднять http-сервер в том же процессе, что и приложение, и в нём точно также рендерить все на странице, которую смотреть с браузера. Понятно, что тут надо подумать, как лучше организовать код чтобы 2 раза не писать рендеринг, но принципиальных препядствий этому я не вижу.


              1. raveclassic
                25.05.2017 10:28

                Вы в своем уме? Вы хотите переписать пол-инфраструктуры, дебаггер и кучу сопровождающих вещей, которые по природе своей асинхронны и писались под асинхронную среду с event loop'ом только для того, чтобы переть поперек паровоза изначального асинхронного дизайна, который не просто так был выбран? Бога ради, зачем вам js? Пишите лучше на чем-нибудь более синхронном.


                1. vintage
                  25.05.2017 10:49

                  "асинхронный дизайн" — самая большая ошибка дизайна JS. Сейчас её пытаются худо бедно исправить через async-await.


                  1. raveclassic
                    25.05.2017 10:56
                    +2

                    Ну, «дизайн JS» вообще звучит как-то нелепо, наверное я погорячился :)

                    Но почему худо бедно? Не вижу в async/await как-то очевидных проблем. Сразу скажу, я не испытываю дискомфорта, когда явно видно, асинхронная функция или нет.


                    1. vintage
                      25.05.2017 12:11

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


                      1. raveclassic
                        25.05.2017 12:19

                        Ну, казалось бы, эффект такой же, как если поменять сигнатуру функции. Тут в помощь TS и компания.


                        1. vintage
                          25.05.2017 12:50
                          -1

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


                          1. raveclassic
                            25.05.2017 13:01
                            +1

                            Ну так если вы хотите поднять обработку этого эффекта (асинхронщины) выше по стэку, вам придется научить все функции работе с этим эффектом. В противном случае он просто затеряется, как это бывает с промисами посреди функции.

                            Edit: Другое дело, что компилятор (TS по крайней мере) это никак не поймает…


                            1. vintage
                              25.05.2017 13:38

                              Так и и не хочу его никуда "поднимать". Я хочу загрузить файл синхронно, но не блокируя интерфейс.


                              1. norlin
                                25.05.2017 13:48

                                Что вам мешает это сделать?


                              1. raveclassic
                                25.05.2017 14:01
                                +1

                                Как вы это сделаете в одном потоке? Не нужно гнать на беднягу JS из-за того, что ему не дается доступ к нескольким потокам.

                                Ну и никто не отменял веб-воркеры для таких задач.


                                1. norlin
                                  25.05.2017 14:09

                                  Кажется, vintage имел в виде нечто более простое.
                                  Если я правильно понял, он имел в виду сделать "синхронный" код отрисовки интерфейса, при этом загрузить какие-то данные асинхронно без блокировки.


                                  Делается элементарно, без переписывания каких-либо сигнатур выше по стеку:


                                  async function fetch(){
                                      let data = await new Promise((done)=>{
                                          setTimeout(()=>done('some_async_data'), 2000);
                                      });
                                  
                                      console.log(`Асинхронный апдейт UI после загрузки данных: ${data}`);
                                  }
                                  
                                  function draw(){
                                      console.log('Синхронная отрисовка UI, шаг 1');
                                  
                                      // даже не нужно знать, синхронная она или нет
                                      // никаких изменений при вызове асинхронной функции...
                                      fetch();
                                  
                                      console.log('Синхронная отрисовка UI без блокировки на загрузку данных');
                                  }
                                  
                                  // ...выше по стеку тоже без изменений
                                  draw();


                                  1. vintage
                                    25.05.2017 16:28

                                    Ваш код не заработает — в этом и проблема :-)


                                    1. norlin
                                      25.05.2017 16:29

                                      $ node async.js
                                      Синхронная отрисовка UI, шаг 1
                                      Синхронная отрисовка UI без блокировки на загрузку данных
                                      Асинхронный апдейт UI после загрузки данных: some_async_data

                                      Что я делаю не так? Попробуйте сами, это не сложно.


                                      1. vintage
                                        25.05.2017 16:30

                                        Вы не выводите полученные данные.


                                        1. norlin
                                          25.05.2017 16:34

                                          Как же не вывожу, вон они выведены.


                                          1. vintage
                                            25.05.2017 16:54

                                            Вы выводите данные в модуле загрузки данных, а надо в функции draw.


                                            1. norlin
                                              25.05.2017 16:55

                                              Эм. А какая разница? Ну сделайте там вместо setTimeout какой-нибудь аякс-вызов – будет абсолютно то же самое.


                                              В общем, дайте код, который у вас работает без async/await и который, по вашему мнению, не будет работать с async/await. А то какой-то диалог ни о чём.


                                              1. vintage
                                                25.05.2017 16:59

                                                1. norlin
                                                  25.05.2017 17:00

                                                  Что именно там надо сравнить? Там диффы из нескольких файлов каждый.


                                                  1. vintage
                                                    25.05.2017 17:03

                                                    Можете сравнить число затронутых файлов или число функций, которые изменили сигнатуру.


                                                    1. norlin
                                                      25.05.2017 17:05

                                                      Вы конкретный пример приведите: "Вот код с файберс, вот аналогичный код на async/await."


                                                      А то я сравню — а вы заявите "да нет, это ж не то совсем". Да и там конфиги затронуты, что вообще отношения не имеет.


                                                      1. vintage
                                                        25.05.2017 17:09

                                                        Пожалуйста: https://habrahabr.ru/post/307288/


                                                        1. norlin
                                                          25.05.2017 17:11

                                                          Там вообще целая статья со всяким разным. Неужели сложно просто два куска кода сюда вставить? Или уже сами поняли, что не правы?


                                                          Просто я вам привёл конкретный пример: для использования async-функции не нужно менять сигнатуры выше. Вы же разводите демагогию...


                                                          1. vintage
                                                            25.05.2017 17:22

                                                            Потрудитесь всё же потратить 10 минут своего бесценного времени на чтение статьи. Там есть ответы на все ваши вопросы. Она не такая уж большая, но на её написание у меня ушёл не один день. Спасибо.


                                                            1. norlin
                                                              25.05.2017 17:25

                                                              Да нет у меня никаких вопросов. И статья эта мне не интересна, спасибо. Тем более, я считаю, что вы не правы. Зачем мне читать статью человека, который, по моему мнению, ошибается и даже не удосуживается как-то обосновать своё мнение.


                                1. vintage
                                  25.05.2017 16:30

                                  В одном потоке это делается через сопрограммы. node-fiber — добавляет их поддержку в ноду. Попробуйте, вам понравится :-)


                                  Веб-воркеры тут ничем не помогут, к сожалению.


                                  1. raveclassic
                                    25.05.2017 16:34

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


                                    1. vintage
                                      25.05.2017 16:56

                                      Там не надо "расставлять обёртки".


                                      1. raveclassic
                                        25.05.2017 17:00

                                        Вы точно про эти файберы?


                                        1. vintage
                                          25.05.2017 17:02

                                          Да, точно. Изменяется только точка старта приложения и точки запуска асинхронных задач. Весь остальной код остаётся неизменным.


                                          1. raveclassic
                                            25.05.2017 17:30

                                            А, вы про это.
                                            Ну, как по мне, так это слишком лихой поворот парадигмы, так как мозги уже давно привыкли к работе с асинхронщиной в том виде, к котором она представлена в js. А тут какая-то синхронизирующая магия, не понятно (вот честно, мне лень разбираться) как работающая. Не с проста же в V8 их не включили.


                                            1. vintage
                                              25.05.2017 17:49

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


                                              1. norlin
                                                25.05.2017 17:57
                                                +1

                                                Ни на что не намекаю, но какой код, по-вашему, имеет меньшее количество "обёрток"?


                                                var Fiber = require('fibers');
                                                
                                                function sleep(ms) {
                                                    var fiber = Fiber.current;
                                                    setTimeout(function() {
                                                        fiber.run();
                                                    }, ms);
                                                    Fiber.yield();
                                                }
                                                
                                                Fiber(function() {
                                                    console.log('wait... ' + new Date);
                                                    sleep(1000);
                                                    console.log('ok... ' + new Date);
                                                }).run();
                                                console.log('back in main');

                                                vs.


                                                async function sleep(ms){
                                                    return await new Promise(done=>setTimeout(done, ms));
                                                }
                                                
                                                (async function(){
                                                    console.log('wait... ' + new Date);
                                                    await sleep(1000);
                                                    console.log('ok... ' + new Date);
                                                }());
                                                console.log('back in main');


                                                1. vintage
                                                  25.05.2017 22:03
                                                  +1

                                                  Одинаково. Добавьте промежуточные вызовы. Для первого варианта ничего не изменится. Второй обрастёт конечными автоматами на каждый вызов.


                                                  1. norlin
                                                    25.05.2017 22:05
                                                    -1

                                                    Ну блин, не смешно ещё? Я вам уже второй пример ошибочности вашего мнения – а вы лишь общими словами отделываетесь. "Протечки в абстракциях" я бы вам чинить точно не доверил.


                                                    1. vintage
                                                      25.05.2017 22:15

                                                      Вы статью-то почитайте, там всё разжёвано.


                                                      1. norlin
                                                        26.05.2017 02:26

                                                        Ещё раз: не нужно ничего разжёвывать. Просто банальный один пример кода приведите. Неужели так сложно? Бесполезных комментариев-то в разы больше уже понаписали.


                                              1. mayorovp
                                                25.05.2017 19:47

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


                                                К примеру, ваша библиотека для реактивного программирования не сможет нормально отслеживать зависимости если не будет проверять текущий поток исполнения. То есть в мире существует как минимум три библиотеки (knockout, mobx и ваша), которые не смогут работать с файберами без переделок.


                                                1. vintage
                                                  25.05.2017 22:14

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


                                                  1. mayorovp
                                                    25.05.2017 22:34

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


                                                    Проблема файберов — в том, что они все эти слои эффективно перемешивают.


                                                    1. vintage
                                                      25.05.2017 22:43

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


                                                      1. mayorovp
                                                        25.05.2017 22:46

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


                                                        1. vintage
                                                          25.05.2017 23:10

                                                          Ага, если какой-нибудь сторонний модуль не решит вдруг вызвать колбэк нашего модуля синхронно, в ответ на наше к нему обращение. Буквально недавно так напоролся на событие blur в браузере, которое всплывало при удалении узла, и долго не понимал "какого фига всё ломается?".


                                                          1. mayorovp
                                                            25.05.2017 23:16

                                                            Когда такие вещи недокументированы, либо документированы но неочевидны — это действительно может быть началом веселой отладки.


                                                            Но даже такие вещи иногда можно закрыть хорошей абстракцией. Например, при использовании mobx можно любые обработчики событий оборачивать в декоратор action, который выключит трекинг зависимостей и включит транзакцию.


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


                                                            1. vintage
                                                              26.05.2017 00:00

                                                              А что там не так с этой ситуацией? Всё отлично работает. Ещё ни разу не встречал проблем с этим.


                                                              1. mayorovp
                                                                26.05.2017 05:51

                                                                А вы часто работали с фиберами?


                                                                1. vintage
                                                                  26.05.2017 08:45
                                                                  +2

                                                                  Конечно, без них очень грустно.


  1. ReklatsMasters
    24.05.2017 13:55
    +4

    Не поленился и дочитал статью. Сложилось впечатление, что автор ранее никогда не писал на js, и пришёл из мира системных языков. Думаю, что именно оттуда такая тяга к последовательному исполнению кода.

    Посмотрел в исходный код, это просто ужас:

    * Один здоровенный файл на over2000 строк, никакой модульности.
    * Только 1 комментарий на всю тонну кода (в конце, может и в середине есть, но это не точно).
    * Расширение прототипа стандартного объекта Array, а это антипаттерн.
    * Вообще нет ни единого стиля кода, ни линтеров.
    * Весь кода написан в ES5, когда на дворе 2017.
    * Нет и намёка на оптимизации — операторы delete, не вынесенные try-catch и прочее.
    * Странные и непонятные тесты.

    Хотел посмотреть, что делает этот модуль под капотом, но после взгляда на сорцы пропало всякое желание с ним связываться.


    1. Ronnie_Gardocki
      25.05.2017 20:09

      > Нет и намёка на оптимизации — операторы delete, не вынесенные try-catch и прочее.
      Справедливости ради, в v8 еще в январе (а скорее и сильно раньше) все как следует прокачали, и теперь try-catch, delete и еще 95% старых деоптимизаторов работают абсолютно без проблем, так можно перестать быть параноиком с этими операторами (сам таким был).


      1. ReklatsMasters
        25.05.2017 21:51

        Эти операторы прокачаны в turbofan, новый оптимизирующий компилятор в V8, он будет включён по дефолту в node@8. Но опять же, если нужна поддержка старых версий браузеров и node, то параноить придётся ещё долго.


  1. hubhito
    24.05.2017 22:06
    +2

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


  1. JPEG
    24.05.2017 22:31
    -1

    Осторожно, далее нет сарказма, возможен вывих чувства юмора.
    Очень сильная работа, респект и уважуха. С такими навыками и пониманием сопряженной работы компилятора и рантайма можно смело идти искать работу на этом поприще, если еще не. Конечно, озвученная выше критика такого подхода вполне оправдана для уровня приложений, но это не может отменить крутости решения, если бы это всё было реализовано на более низком уровне, чем либа для веба — тогда у нас был бы lua, или python, или ruby, которые нативно умеют в coroutines. Лайкнул, в общем.