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кб без минимизации),
- не имеет зависимостей,
- не требует компиляции,
- исполняется значительно быстрее,
- позволяет запускать и останавливать долгоживущие потоки.
Для иллюстрации разберем небольшой пример веб-приложения, которое:
- Получает список файлов через ajax-запрос
- Для каждого файла из списка:
- Получает файл через ajax-запрос
- Пишет содержимое файла на страницу
- Ждет 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)
Hazrat
24.05.2017 00:13+4Столько сил потрачено зря, действительно async/await чем не синхронность?
arvitaly
24.05.2017 00:50Тем, что не позволяет скрывать реализацию функции, вот была у нас функция синхронной мы писали без await, стала синхронной мы должны везде изменить на await, в итоге процентов 20 высокоуровневого кода становится заполнено ключевым словом «await».
По проекту, задумка интересна, но ИМХО, это должно решаться автоматически, т.е. компилятор видит, что функция асинхронна и всегда «подставляет» await. Если же хотим получить Promise из нее и выполнить асинхронно, пишем специальную функцию (по опыту, таких ситуаций 0.1%).Hazrat
24.05.2017 01:10Из за этого вы решили разрабатывать этот проект? В ES6 можно использовать стрелочные функции, ушло много мешанины, и я думаю для некоторых асинхронных функций пометка async не то чтобы нагружает, наоборот помечает, что это асинхронная, что она возвращает промис. Ну не знаю, не представляю пока, где бы я ее применил бы.
amaksr
24.05.2017 01:32это должно решаться автоматически, т.е. компилятор видит, что функция асинхронна и всегда «подставляет» awai
Согласен, именно так и должно быть. Вместо этого разработчики языка всем парят, что генераторы/промисы/async/await это круто и теперь надо всем учится программировать по-новому: массивы перебирать рекурсией, желательно хвостовой, и вообще переходить на ФП. Приходится за них это делать то, что должно было быть сделано много лет назад.raveclassic
24.05.2017 02:19Недавно тут штудировал esdiscuss на тему do notation, которая позволила бы решить проблемы с синтаксисом при использовании монад, которые, в свою очередь, прекрасно справляются с описанной вами проблемой. Ну что уж, выводы неутешительные, конечно, хотя там сам Брендан Айк написал год назад «We'll get it»!
mayorovp
24.05.2017 14:02-1Для языка с динамической типизацией это невозможно — любая функция может оказаться как синхронной, так и асинхронной.
vintage
24.05.2017 14:39Возможно. Нужно уметь замораживать и размораживать текущий стек вызовов. Делать это можно либо через node-fibers, либо через бросание исключения + реактивное программирование.
mayorovp
24.05.2017 17:30-1Как вы себе представляете замораживание стека вызовов компилятором?
vintage
24.05.2017 17:39-1Компилятором — никак. Это в рантайме делается.
arvitaly
25.05.2017 05:00vintage
Ну я не соглашусь, менять runtime javascript выйдет дороже, поэтому реально выстрелил typescript, babel и flow, а не куча компиляторов с тяжеленным или не кроссплатформенным рантаймом.
mayorovp
Компилятор вполне способен выводить типы, определять нужные ветви кода из чужого модуля (tree shaking), ну и никто не запрещает ввести ограничения на такой язык (которые так и так будут полезны).
Другое дело, как это сделать красиво и явно для разработчиков, тут нужно менять и технологии, и культуру.vintage
25.05.2017 09:05Что пиарят — то и выстреливает. node-fibers — вполне себе лёгкий рантайм. Лучше бы его стандартизировали, а не async/await. $mol_atom — кроссплатформенная либа кило на 10, но она больше про реактивное программирование, а синхронный код — приятный бонус.
LestaD
31.05.2017 20:06+2В вашем комментарии скрыта огромная проблема — неопытность в проектировании больших проектов. Ни один человек в здравом уме не будет менять функцию/метод настолько, чтобы потребовалась асинхронщина.
В данном случае правильнее будет написать новую асинхронную фукцию, чтобы как раз, не ломать совместимость с предыдущим кодом. Введение этого «движка» в проект только ухудшит поддержку кода, по нескольким причинам:
— Новый слой абстракции. Это всегда проблема, когда этот слой не нужен.
— Неизвестная технология. Как новичкам, так и старичкам в проекте придется разбираться в работе этого модуля, придется исследовать баги, ждать пока Вы их поправите, или же править их самому.
— Не стандарт. Этого нет в ECMAScript
comerc
24.05.2017 01:18+2Ключевой вопрос "Зачем?" — не нашел ответа.
raveclassic
24.05.2017 01:26Что только люди не придумают… А ведь просили же, дайте нам инструмент для работы с асинхронностью. Ну дали. А воз и ныне там.
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); }; });
amaksr
24.05.2017 08:44Да, недостатки те же, что и у async/await:
1. Если какая-то функция внизу стека стала async-await (или yield), то все вызывающие функции, и весь граф вызовов, надо также менять на async-await. Я считаю это неправильно, когда программист должен отвлекаться на такие вещи.
2. Несовместимо с некоторыми браузерами (только через babel)norlin
24.05.2017 09:13+1Кстати, спасибо: благодаря комментариям я внезапно узнал, что node.js уже нативно поддерживает async/await и отказался от
co
. Смысла городить велосипеды я не вижу, честно. В вашем случае всяких обёрток и прочей лишней работы приходится делать больше, чем в случае с async/await.
vintage
24.05.2017 09:12+3Не хотите ли добавить своё решение в коллекцию? https://github.com/nin-jin/async-js/
amaksr
26.05.2017 08:12Вот такое решение получилось:
index.jsvar 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'); });
vintage
26.05.2017 08:42require('../../nsynjs');
Может опубликуете в NPM?
require.main.require( './user' );
А почему не просто
require( './user' )
?
exports.readFile = function (ctx,name) {
Может сделать это универсальным враппером идущим с самой библиотекой?
const readFileSync = synjs.fromAsync( fs.readFile )
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 нужно только для синхронного кода.
Может сделать это универсальным враппером идущим с самой библиотекой?
Да, наверное можно врапперы нескольких самых общеупотребительных функций в нее включитьvintage
26.05.2017 10:35+1Резолвить относительно мейна — тоже не вариант. Я так понимаю вы парсите переданную функцию, транспилируете код и эвалите в контексте своего модуля? В этот момент можно подменять
require
наmodule.require
того модуля, откуда взят код. Передать модуль можно, например, так:nsynjs.run(module,synchronousApp)
amaksr
26.05.2017 19:27Да, модули можно передавать в параметрах в синхронный код. Это так и сделано, например в файле user.js:
... var wrappers = require('./wrappers'); nsynjs.run(synchronousCode,{},wrappers,function (m) { module.exports = m; });
Можно и так, и так, кому как больше нравится.
В нативном require в node модули можно тоже грузить несколькими способами.vintage
26.05.2017 20:20+1Нужно всё же такое решение, которое позволит по минимуму отступать от обычного кода.
mayorovp
24.05.2017 10:57-2По-сравнению с Babel он:
- исполняется значительно быстрее,
Тем, кто просто ищет решение, способное выполняться быстрее чем Babel, могу порекомендовать попробовать tsc (Typescript Compiler).
justboris
24.05.2017 12:17+2Ставить в достоинства маленький размер по сравнению с babel некорректно, потому что babel никто в браузер не грузит, а прогоняет код на этапе сборки.
justboris
24.05.2017 12:19+2А еще, я попробовал поставить breakpoint в как-бы-синхронной функции, а он не сработал.
Как отлаживать такой код?
amaksr
24.05.2017 17:31Дебаггера под это пока нет. При надобности может сделаю, это будет не такая большая работа, по сравнению с уже проделанной.
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
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'); });
Это связано с тем, что функции, определенные внутри синхронного кода, не работают с нативными переменными, а вместо этого модифицируются движком и используют хэш в контексте потока. Поэтому тело такой функции можно использовать только внутри синхронного кода.raveclassic
24.05.2017 20:22+5Ну вот ваше решение и начинает превращаться в тыкву. «Решив» одну проблему, вы принесли ворох новых — появились какие-то сомнительные ограничения.
amaksr
24.05.2017 21:22-1А как вы планируете его делать?
Дебаггер можно отображать на динамическом div-е. Для этого нужно для каждого элемента языка в прототипы добавить функцию, которая бы выводила в смотрибельном виде данные о текущем состоянии из контекста. Ну и кнопки пошагового исполнения добавить. В общем это тривиальная задача по отображению разнородных данных из дерева на странице.raveclassic
25.05.2017 00:43+3Дебаггер можно отображать на динамическом div-е. Для этого нужно для каждого элемента языка в прототипы добавить функцию
Вы бы себя слышали :)
Ох… Ну а если я под Node?amaksr
25.05.2017 03:13-3Вы бы себя слышали :)
А что я такого сказал? Структура с данными есть, контроль над исполнением тоже есть, причем полный. Исполняем шаг — рендерим структуру. В чем проблема?
Ох… Ну а если я под Node?
Под нодой можно поднять http-сервер в том же процессе, что и приложение, и в нём точно также рендерить все на странице, которую смотреть с браузера. Понятно, что тут надо подумать, как лучше организовать код чтобы 2 раза не писать рендеринг, но принципиальных препядствий этому я не вижу.raveclassic
25.05.2017 10:28Вы в своем уме? Вы хотите переписать пол-инфраструктуры, дебаггер и кучу сопровождающих вещей, которые по природе своей асинхронны и писались под асинхронную среду с event loop'ом только для того, чтобы переть поперек
паровозаизначального асинхронного дизайна, который не просто так был выбран? Бога ради, зачем вам js? Пишите лучше на чем-нибудь более синхронном.vintage
25.05.2017 10:49"асинхронный дизайн" — самая большая ошибка дизайна JS. Сейчас её пытаются худо бедно исправить через async-await.
raveclassic
25.05.2017 10:56+2Ну, «дизайн JS» вообще звучит как-то нелепо, наверное я погорячился :)
Но почему худо бедно? Не вижу в async/await как-то очевидных проблем. Сразу скажу, я не испытываю дискомфорта, когда явно видно, асинхронная функция или нет.vintage
25.05.2017 12:11Дискомфорт начинается, когда синхронную функцию, которая много где используется, нужно вдруг сделать асинхронной и для этого нужно перелопатить половину проекта, превращая половину остальных функций в асинхронные.
raveclassic
25.05.2017 12:19Ну, казалось бы, эффект такой же, как если поменять сигнатуру функции. Тут в помощь TS и компания.
vintage
25.05.2017 12:50-1Нет, тут всё куда печальнее — нужно менять сигнатуры всех функций, что могут оказаться выше по стеку вызовов.
raveclassic
25.05.2017 13:01+1Ну так если вы хотите поднять обработку этого эффекта (асинхронщины) выше по стэку, вам придется научить все функции работе с этим эффектом. В противном случае он просто затеряется, как это бывает с промисами посреди функции.
Edit: Другое дело, что компилятор (TS по крайней мере) это никак не поймает…vintage
25.05.2017 13:38Так и и не хочу его никуда "поднимать". Я хочу загрузить файл синхронно, но не блокируя интерфейс.
raveclassic
25.05.2017 14:01+1Как вы это сделаете в одном потоке? Не нужно гнать на беднягу JS из-за того, что ему не дается доступ к нескольким потокам.
Ну и никто не отменял веб-воркеры для таких задач.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();
vintage
25.05.2017 16:28Ваш код не заработает — в этом и проблема :-)
norlin
25.05.2017 16:29$ node async.js Синхронная отрисовка UI, шаг 1 Синхронная отрисовка UI без блокировки на загрузку данных Асинхронный апдейт UI после загрузки данных: some_async_data
Что я делаю не так? Попробуйте сами, это не сложно.
vintage
25.05.2017 16:30Вы не выводите полученные данные.
norlin
25.05.2017 16:34Как же не вывожу, вон они выведены.
vintage
25.05.2017 16:54Вы выводите данные в модуле загрузки данных, а надо в функции draw.
norlin
25.05.2017 16:55Эм. А какая разница? Ну сделайте там вместо
setTimeout
какой-нибудь аякс-вызов – будет абсолютно то же самое.
В общем, дайте код, который у вас работает без async/await и который, по вашему мнению, не будет работать с async/await. А то какой-то диалог ни о чём.
vintage
25.05.2017 16:59Сравните:
https://github.com/nin-jin/async-js/compare/sync...async-await-babel
https://github.com/nin-jin/async-js/compare/sync...async-fibersnorlin
25.05.2017 17:00Что именно там надо сравнить? Там диффы из нескольких файлов каждый.
vintage
25.05.2017 17:03Можете сравнить число затронутых файлов или число функций, которые изменили сигнатуру.
norlin
25.05.2017 17:05Вы конкретный пример приведите: "Вот код с файберс, вот аналогичный код на async/await."
А то я сравню — а вы заявите "да нет, это ж не то совсем". Да и там конфиги затронуты, что вообще отношения не имеет.
vintage
25.05.2017 17:09Пожалуйста: https://habrahabr.ru/post/307288/
norlin
25.05.2017 17:11Там вообще целая статья со всяким разным. Неужели сложно просто два куска кода сюда вставить? Или уже сами поняли, что не правы?
Просто я вам привёл конкретный пример: для использования async-функции не нужно менять сигнатуры выше. Вы же разводите демагогию...
vintage
25.05.2017 17:22Потрудитесь всё же потратить 10 минут своего бесценного времени на чтение статьи. Там есть ответы на все ваши вопросы. Она не такая уж большая, но на её написание у меня ушёл не один день. Спасибо.
norlin
25.05.2017 17:25Да нет у меня никаких вопросов. И статья эта мне не интересна, спасибо. Тем более, я считаю, что вы не правы. Зачем мне читать статью человека, который, по моему мнению, ошибается и даже не удосуживается как-то обосновать своё мнение.
vintage
25.05.2017 16:30В одном потоке это делается через сопрограммы. node-fiber — добавляет их поддержку в ноду. Попробуйте, вам понравится :-)
Веб-воркеры тут ничем не помогут, к сожалению.
raveclassic
25.05.2017 16:34Файберы выглядят, конечно, прикольно, только вот мне не особо по душе расставлять в коде обертки для их работы. С другой стороны, именно этим я в сагах на клиенте и занимаюсь.
vintage
25.05.2017 16:56Там не надо "расставлять обёртки".
raveclassic
25.05.2017 17:00Вы точно про эти файберы?
vintage
25.05.2017 17:02Да, точно. Изменяется только точка старта приложения и точки запуска асинхронных задач. Весь остальной код остаётся неизменным.
raveclassic
25.05.2017 17:30А, вы про это.
Ну, как по мне, так это слишком лихой поворот парадигмы, так как мозги уже давно привыкли к работе с асинхронщиной в том виде, к котором она представлена в js. А тут какая-то синхронизирующая магия, не понятно (вот честно, мне лень разбираться) как работающая. Не с проста же в V8 их не включили.vintage
25.05.2017 17:49Прелесть этой магии в том, что вам и не нужно разбираться как она работает. Она просто работает как надо. Вы пишите простой и понятный синхронный код, но он не блокирует системный поток.
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');
vintage
25.05.2017 22:03+1Одинаково. Добавьте промежуточные вызовы. Для первого варианта ничего не изменится. Второй обрастёт конечными автоматами на каждый вызов.
norlin
25.05.2017 22:05-1Ну блин, не смешно ещё? Я вам уже второй пример ошибочности вашего мнения – а вы лишь общими словами отделываетесь. "Протечки в абстракциях" я бы вам чинить точно не доверил.
mayorovp
25.05.2017 19:47Вот только у этого "простого и понятного" синхронного кода будут все недостатки многопоточного кода — неожиданно меняющиеся разделяемые переменные, следующие отсюда состояния гонки и т.п.
К примеру, ваша библиотека для реактивного программирования не сможет нормально отслеживать зависимости если не будет проверять текущий поток исполнения. То есть в мире существует как минимум три библиотеки (knockout, mobx и ваша), которые не смогут работать с файберами без переделок.
vintage
25.05.2017 22:14В случае моей библиотеки никакой гонки не будет. В случае файберов — да, возможно. Но ручное указание повсюду await-ов вас никак не убережёт от изменения разделяемой переменной обычной синхронной функцией, которую вы вызвали между чтением значения и записью в него.
mayorovp
25.05.2017 22:34От этого убережет разделение на слои. При хорошей архитектуре становится очевидно, какой вызов может привести к повторному входу, а какой — нет.
Проблема файберов — в том, что они все эти слои эффективно перемешивают.
vintage
25.05.2017 22:43Так дело тут не в "повторном входе", а в "разделяемом состоянии", которое может поменяться и без какого-либо "выхода".
mayorovp
25.05.2017 22:46Состояние, разделяемое между функциями модуля, не может поменяться без повторного входа в модуль.
vintage
25.05.2017 23:10Ага, если какой-нибудь сторонний модуль не решит вдруг вызвать колбэк нашего модуля синхронно, в ответ на наше к нему обращение. Буквально недавно так напоролся на событие blur в браузере, которое всплывало при удалении узла, и долго не понимал "какого фига всё ломается?".
mayorovp
25.05.2017 23:16Когда такие вещи недокументированы, либо документированы но неочевидны — это действительно может быть началом веселой отладки.
Но даже такие вещи иногда можно закрыть хорошей абстракцией. Например, при использовании mobx можно любые обработчики событий оборачивать в декоратор action, который выключит трекинг зависимостей и включит транзакцию.
Но даже такие ошибки это не сравнить с ситуацией, когда любой модуль может случайно передать управление любому другому модулю через механизм фиберов.
ReklatsMasters
24.05.2017 13:55+4Не поленился и дочитал статью. Сложилось впечатление, что автор ранее никогда не писал на js, и пришёл из мира системных языков. Думаю, что именно оттуда такая тяга к последовательному исполнению кода.
Посмотрел в исходный код, это просто ужас:
* Один здоровенный файл на over2000 строк, никакой модульности.
* Только 1 комментарий на всю тонну кода (в конце, может и в середине есть, но это не точно).
* Расширение прототипа стандартного объекта Array, а это антипаттерн.
* Вообще нет ни единого стиля кода, ни линтеров.
* Весь кода написан в ES5, когда на дворе 2017.
* Нет и намёка на оптимизации — операторы delete, не вынесенные try-catch и прочее.
* Странные и непонятные тесты.
Хотел посмотреть, что делает этот модуль под капотом, но после взгляда на сорцы пропало всякое желание с ним связываться.Ronnie_Gardocki
25.05.2017 20:09> Нет и намёка на оптимизации — операторы delete, не вынесенные try-catch и прочее.
Справедливости ради, в v8 еще в январе (а скорее и сильно раньше) все как следует прокачали, и теперь try-catch, delete и еще 95% старых деоптимизаторов работают абсолютно без проблем, так можно перестать быть параноиком с этими операторами (сам таким был).ReklatsMasters
25.05.2017 21:51Эти операторы прокачаны в turbofan, новый оптимизирующий компилятор в V8, он будет включён по дефолту в node@8. Но опять же, если нужна поддержка старых версий браузеров и node, то параноить придётся ещё долго.
hubhito
24.05.2017 22:06+2самая большая проблема с такими библиотеками — в дальнейшем код будет сложно перевести на те технологии которые уже скоро будут базовыми. сила babel ещё и в том что потом нужно будет просто компилятор отключить и все будет работать
JPEG
24.05.2017 22:31-1Осторожно, далее нет сарказма, возможен вывих чувства юмора.
Очень сильная работа, респект и уважуха. С такими навыками и пониманием сопряженной работы компилятора и рантайма можно смело идти искать работу на этом поприще, если еще не. Конечно, озвученная выше критика такого подхода вполне оправдана для уровня приложений, но это не может отменить крутости решения, если бы это всё было реализовано на более низком уровне, чем либа для веба — тогда у нас был бы lua, или python, или ruby, которые нативно умеют в coroutines. Лайкнул, в общем.
mwizard
Стоило ли делать все это ради неявного async/await с переусложненным интерфейсом? В современном javascript не используются коллбэки, насколько мне известно — их место заняли Promise, которые, в свою очередь, пишутся в псевдосинхронном стиле через await.
amaksr
Промисы не сильно помогают если их нужно связать логикой, и выбор последующего шага зависит от данных с предыдущего.
А async/await все еще не доступен на многих браузерах, особенно в крупных компаниях. Ну не отказывать же им, если они требуют совместимости с IE…
mwizard
Казалось бы, именно ради этого и был написан babel...
bano-notit
А этот движок прям сразу в крупных компаниях возьмут на вооружение...
hubhito
Есть такая штука как co https://github.com/tj/co
позволяет в том числе подготовить код к переходу на async await. кроме того поддерживает thunk, т.е. можно работать со старым кодом на колбэках как если бы это были промисы
hubhito
ну и к нему нужен Babel, да, чтобы использовать генераторы
norlin
В ноде – не нужен. Но в ноде и async/await уже можно (начиная с 7.6)...
amaksr
В процессе более активного использования nsynjs список отличий от async/await сформировался такой:
— в nsynjs нет надобности использовать ключевые слова async/await в коде, так как тип исполняемой функции проверяется в рантайме
— в nsynjs отпадает надобность в промисах в принципе (хотя для поклонников можно добавить несколько строчек в код nsynjs чтобы проверять возвращаемый функцией результат в рантайме на предмет не промис ли он, и не надо ли подождать).
— в nsynjs для исключительных ситуаций достаточно механизма try/catch/throw. Механизм Promise/then/catch/reject не нужен.
Преимущества по-сравнению с async/await:
— возможность запускать псевдо-потоки,
— возможнось останавливать псевдо-потоки как изнутри, так и извне,
— возможнось подчистить активные функции с колбеками при остановке потока извне (например на активный setTimeout автоматически вызвать clearTimeout),
— возможность создавать конструкторы с асинхронными функциями внутри.
mayorovp
Что такое "возможность запускать псевдо-потоки" и почему это преимущество перед async/await?
mwizard
и это плохо. Javascript неспроста движется в сторону проверки всего и вся при компиляции, включая статическую типизацию.
прелесть промисов в явном, детерминированном управлении event loop — см. концепции greenlets, fibers и т.д. В nsynjs этого нет, и по дизайну невозможно.
Наверное, я неправильно использовал промисы все это время?
В чем отличие от запущенной async-функции?
В чем отличие от bluebird, который умеет отменять исполняющиеся промисы?
В чем отличие от обертки try/finally внутри тела async-функции, с clearTimeout внутри finally?
А за асинхронные конструкторы, как по уму, надо бы отрывать руки по самые ягодицы. Как, например, за асинхронные геттеры, асинхронные сеттеры, асинхронные присваивания и т.д. Если нужно сконструировать объект асинхронно — сделай статический метод-фабрику, который соберет всю нужную информацию и передаст ее в синхронный конструктор, так как объекты должны быть копируемыми. Конструктор с сайд-эффектами — это в принципе признак омерзительного дизайна.
amaksr
В JS изначально даже var не было, да и сейчас практически все резольвятся в рантайме, поэтому я бы не стал брать JS в качестве примера как надо делать типизацию.
в nsynjs свой event loop, а также свои структуры с программными счетчиками, стеками, локальными переменными, closures и т.п.
Имеется ввтиду внутри промисифицированных функций, или если промис вернули куда-то в не async-функцию
в том что есть указатель на ее состояние, с её собственным event-loop-ом, по которому над ней можно иметь полный контроль.
В блюберд придется писать код чтобы отслеживать активные промисы. Хотя это и не сложно. В нативных промисах этого нет, и, говорят, не будет.
Это придется делать везде, где вызывается промис с setTimeout? Либо делать async-обертку к промису, ну и отслеживать активные обертки. В nsynjs это делается автоматически, т.к. есть свой стек, по которому можно всегда узнать что сейчас активно.
Не буду холиварить. Видел их много раз, и не сказал бы, что изза них какие-то существенные проблемы. Это как goto, кому-то нравится, кому-то нет…
mwizard
В JS изначально много чего не было. Тот JS, который имеем сейчас, и тот, с которого начиналось, это два разных языка, с разными ценностями и идиомами. И современный JS семимильными шагами идет к сильной типизации с выведением типов.
И все это, конечно же, неявное. И, кстати, зачем это все нужно, ведь рантайм уже все предоставляет?
Если вернули не в async-функцию, она с ним работать все равно не сможет, поэтому и исключение ловить не надо, т.к. исключение все равно не будет выброшено в контексте этой функции.
Насколько мне известно, запустить несколько event loop в одном потоке нельзя по определению этого самого event loop, т.к. каждый event loop должен выполнять ожидание событий на своем списке дескрипторов средствами операционной системы. Значит, каждая функция работает в отдельном потоке? Великолепно! И в таком случае, в чем отличие от WebWorkers? И как у вас решился вопрос отсутствия в JS любых механизмов многопоточной синхронизации?
Имеющийся proposal был отозван из-за излишней сложности реализации в v8, насколько мне известно. придумают способ проще — будет.
"Explicit is better than implicit. Simple is better than complex." Я не понимаю, в чем проблема явно освободить занятый ресурс (да, таймер это ресурс), как не вижу проблемы в том, чтобы закрыть за собой сокет или файл. Что делать, если я хочу из nsynjs передать объект таймера за пределы вашей RAII-процедуры? Мне его убьет при выходе из процедуры, несмотря на то, что референс утек? Если не убьет, тогда в чем смысл? Если убьет, то как этим пользоваться?
Ну и я не понимаю, зачем в принципе нужно "отслеживать активные обертки". Асинхронные процедуры на то и асинхронные, что это не потоки, и не должны ими быть — асинхронные процедуры по определению исполняются в одном потоке посредством кооперативной многозадачности. В отслеживании и убийстве асинхронных процедур я вижу непонимание этого факта и попытку сделать менеджер процессов для асинхронных процедур. Но у рантайма нет задачи управлять "процессами", более того — в ни одном языке программирования невозможно чисто завершить тред, так как убив тред, программа автоматически переходит в неопределенное состояние (кроме случаев, когда треды не имели никаких общих данных, никаких глобальных ресуров и т.д.).
Мне бы хотелось больше узнать о мотивации, зачем это было сделано, и какие именно задачи это призвано решать, потому что, как мне кажется, очевидно, что с async/await это решение не конкурент.
vintage
Они могут вкладываться друг в друга. Один event-loop может быть задачей в рамках другого event-loop. Очевидно, тут именно такой случай.
Erlang, D
amaksr
Event loop в nsynjs это просто цикл while(...) {...}, который выполняется в главном jS-потоке. Этот цикл выполняет свои тики пока не встретит вызов функции. Указатель на функцию анализируется на предмет типа функции и надо ли подождать, и если надо, то просто цикл останавлиавается по break. Это похоже на невытесняющую многозадачность, но все происходит внутри одного процесса JS. Поэтому вебворкеры здесь вообще ни при чем.
в случае, если псевдопоток nsynjs завершается извне (типа как по SIGHUP), и надо освободить ресурсы, инача они вызовут колбеки. Стандартный JS такие возможности не предоставляет, ну тоесть надо писать учет активных функций самому. Но раз уж у нас есть свой event-loop и свои стеки, то почему бы не реализовать чтобы это делалось автоматически?vintage
Нет, эти концепции не требуют никакой "явности". Вообще, странно слышать термин "явность" от человека, использующего async, который неявно превращает функцию в конечный автомат.