Считается, что мир JavaScript бурно развивается: регулярно выходят новые стандарты языка, появляются новые синтаксические фишки, а разработчики моментально все это адаптируют и переписывают свои фреймворки, библиотеки и прочие проекты с тем, чтобы все это использовалось. Сейчас, например, если вы всё ещё пишете в коде var, а не const или let, то это уже вроде как моветон. А уж если функция описана не через стрелочный синтаксис, то вообще позор…
Однако, все эти const-ы, let-ы, class-ы и большинство других нововведений не более чем косметика, которая хоть и делает код красивее, но действительно острых проблем не решает.
Я думаю, что основная проблема JavaScript, которая уже давным давно созрела и перезрела, и которая должна была быть решена в первую очередь, это невозможность приостановить выполнение, и как следствие, необходимость все делать через callbacks.
Чем хороши callbacks?
На мой взгляд только тем, что дают нам событийность и асинхронность, что позволяет мгновенно реагировать на события, проделывать большую работу в одном процессе, экономить ресурсы и т.п.
Чем плохи callbacks?
Первое, с чем обычно сталкивается новичок, это тот факт, что с ростом сложности код быстро превращается в малопонятные многократно вложенные блоки — «callback hell»:
fetch(“list_of_urls”, function(array_of_urls){
for(var i=0; array_of_urls.length; i++) {
fetch(array_of_urls[i], function(profile){
fetch(profile.imageUrl, function(image){
...
});
});
}
});
Во-вторых, если функции с колбеками соединены друг с другом логикой, то эту логику приходится дробить и выносить в отдельные именованные функции или модули. Например, код выше выполнит цикл «for» и запустит множество fetch(array_of_urls[i]... мгновенно, и если array_of_urls слишком большой, то движок JavaScript зависнет и/или упадет с ошибкой.
С этим можно бороться путем переписывания цикла «for» в рекурсивную функцию с колбеком, но рекурсия может переполнить стек и также уронить движок. Кроме того, рекурсивные программы труднее для понимания.
Другие пути решения требуют использования дополнительных инструментов или библиотек:
- Promises – позволяет писать код колбеков внутри неких объектов. В результате это те же колбеки, но меньшей вложенности и соединенные друг с другом в цепочки:
firstMethod().then(secondMethod).then(thirdMethod);
На мой взгляд Promises это костыль, потому что
- цепочки вызывают функции только в одном заданном порядке,
- если порядок может менятся в соответсвии с какой-то логикой, по-прежнему приходится дробить логику в колбеках на отдельные функции,
- для кодирования логики между функциями по-прежнему приходится что-то изобретать, вместо того, чтобы просто пользоваться стандартными операторами if, for, while и т.п.
- логика с Promises выглядит малопонятно.
- async (библиотека) — позволяет объявить массив функций с колбеками, и исполнять их одну за другой, или одновременно. Недостатки те же, что и у Promises.
- async/await – новая возможность в JavaScript, основанная на generators, позволяет останавливать и возобновлять исполнение функции.
Будущее, судя по всему, за async/await, но пока это будущее не наступило, и многие движки эту возможность не поддерживают.
Чтобы иметь возможность исполнять код с async/await на актуальных на данный момент движках JavaScript 2015, были созданы транспиляторы — преобразователи кода из нового JavaScript в старый. Самый известный из них, Babel, позволяет конвертировать код Javascript 2017 с async/await в JavaScript 2015 и запускать его на практически всех используемых в данный момент движках.
Выглядит это примерно так:
Исходный код на JavaScript 2017:
async function notifyUserFriends(user_id) {
var friends = await getUserFriends(user_id);
for(var i=0; i<friends.length; i++) {
friend = await getUser(friends[i].id);
var sent = await sendEmail(freind.email,"subject","body");
}
}
Конвертированный код на JavaScript 2015:
"use strict";
var notifyUserFriends = function () {
var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee(user_id) {
var friends, i, sent;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return getUserFriends(user_id);
case 2:
friends = _context.sent;
i = 0;
case 4:
if (!(i < friends.length)) {
_context.next = 14;
break;
}
_context.next = 7;
return getUser(friends[i].id);
case 7:
friend = _context.sent;
_context.next = 10;
return sendEmail(freind.email, "subject", "body");
case 10:
sent = _context.sent;
case 11:
i++;
_context.next = 4;
break;
case 14:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));
return function notifyUserFriends(_x) {
return _ref.apply(this, arguments);
};
}();
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
Чтобы иметь возможность отлаживать такой код, необходимо настроить и задействовать многое из того, что перечислено в этой статье.
Всё это само по себе требует нетривиальных усилий. Кроме того, Babel тянет за собой около 100 кб минифицированного кода «babel-polyfill», а сконвертированный код работает медленно (на что косвенно намекают многочисленные конструкции case номер_строки в сгенерированном коде).
Посмотрев на все это, я решил написать свой велосипед — SynJS. Он позволяет писать и синхронно исполнять код с колбеками:
function myTestFunction1(paramA,paramB) {
var res, i = 0;
while (i < 5) {
setTimeout(function () {
res = 'i=' + i;
SynJS.resume(_synjsContext); // < –- функция для сигнализации, что колбек закончен
}, 1000);
SynJS.wait(); // < – оператор, останавливающий исполнение
console.log(res, new Date());
i++;
}
return "myTestFunction1 finished";
}
Исполнить функцию можно следующим образом:
SynJS.run(myTestFunction1,null, function (ret) {
console.log('done all:', ret);
});
Результат будет такой:
i=0 Wed Dec 21 2016 11:45:33 GMT-0700 (Mountain Standard Time)
i=1 Wed Dec 21 2016 11:45:34 GMT-0700 (Mountain Standard Time)
i=2 Wed Dec 21 2016 11:45:35 GMT-0700 (Mountain Standard Time)
i=3 Wed Dec 21 2016 11:45:36 GMT-0700 (Mountain Standard Time)
i=4 Wed Dec 21 2016 11:45:37 GMT-0700 (Mountain Standard Time)
По-сравнению с Babel он:
- легче (35кб без минимизации),
- не имеет зависимостей,
- не требует компиляции,
- исполняется примерно в 40 раз быстрее (хотя это может быть не так критично при работе с медленными функциями).
SynJS берет указатель на функцию в качестве параметра, парсит эту функцию на отдельные операторы (парсит вложенные операторы рекурсивно, если необходимо), оборачивает их все в функции, и помещает эти функции в древовидную структуру, эквивалентную коду функции. Затем создается контекст исполнения, в котором хранится локальные переменные, параметры, текущее состояние стека, программные счётчики и другая информация, необходимая для остановки и продолжения выполнения. После этого операторы в древовидной структуре исполняются один за другим, используя контекст в качестве хранилища данных.
Функция может быть выполнена через SynJS следующим образом:
SynJS.run(funcPtr,obj, param1, param2 [, more params],callback)
Параметры:
— funcPtr: указатель на функцию, которую надо выполнит синхронно
— obj: объект, который будет доступен в функции через this
— param1, param2: параметры
— callback: функция, которая будет выполнена по завершении
Чтобы можно было дожидаться завершения колбека в SynJS существует оператор SynJS.wait(), который позволяет остановить исполнение функции, запущенной через SynJS.run(). Оператор может принимать 3 формы:
— SynJS.wait() — останавливает исполнение пока не будет вызван SynJS.resume()
— SynJS.wait(number_of_milliseconds) – приостанавливает исполнение на время number_of_milliseconds
— SynJS.wait(some_non_numeric_expr) – проверяет (!!some_non_numeric_expr), и останавливает исполнение в случае false.
С помощью SynJS.wait можно ожидать завершения одного или нескольких колбеков:
var cb1, cb2;
setTimeout(function () {
cb1 = true;
SynJS.resume(_synjsContext);
}, 1000);
setTimeout(function () {
cb2 = true;
SynJS.resume(_synjsContext);
}, 2000);
SynJS.wait(cb1 && cb2);
Чтобы дать сигнал о завершении колбека в основной поток используется функция
SynJS.resume(context)
Обязательный параметр context содержит ссылку на контекст исполнения, который необходимо уведомить (так как каждый вызов SynJS.run создает и запускает отдельный контекст, в системе может существовать одновременно несколько запущенных контекстов).
При парсинге SynJS оборачивает каждый оператор оборачивается в функцию следующим образом:
function(_synjsContext) {
... код оператора ...
}
Таким образом можно использовать параметр _synjsContext в коде колбека для сигнализации о завершении:
SynJS.resume(_synjsContext);
Обработка локальных переменных.
При парсинге тела функции SynJS определяет декларации локальных переменных по ключевому слову var, и создаёт для них хеш в контексте исполнения. При обёртывании в функцию код оператора модифицируется, и все ссылки на локальные переменные заменяются ссылками на хеш в контексте исполнения.
Например, если исходный оператор в теле функции выглядел так:
var i, res;
...
setTimeout(function() {
res = 'i='+i;
SynJS.resume(_synjsContext);
},1000);
то оператор, обернутый в функцию будет выглядеть так:
function(_synjsContext) {
setTimeout(function() {
_synjsContext.localVars.res = 'i='+_synjsContext.localVars.i;
SynJS.resume(_synjsContext);
},1000);
}
Несколько примеров использования SynJS
1. Выбрать из БД массив родительских записей, для каждой из них получить список детей.
2. По списку URL-ов, получать их один за другим, пока содержимое URL-а не будет удовлетворять условию.
var SynJS = require('synjs');
var fetchUrl = require('fetch').fetchUrl;
function fetch(context,url) {
console.log('fetching started:', url);
var result = {};
fetchUrl(url, function(error, meta, body){
result.done = true;
result.body = body;
result.finalUrl = meta.finalUrl;
console.log('fetching finished:', url);
SynJS.resume(context);
} );
return result;
}
function myFetches(modules, urls) {
for(var i=0; i<urls.length; i++) {
var res = modules.fetch(_synjsContext, urls[i]);
SynJS.wait(res.done);
if(res.finalUrl.indexOf('github')>=0) {
console.log('found correct one!', urls[i]);
break;
}
}
};
var modules = {
SynJS: SynJS,
fetch: fetch,
};
const urls = [
'http://www.google.com',
'http://www.yahoo.com',
'http://www.github.com', // This is the valid one
'http://www.wikipedia.com'
];
SynJS.run(myFetches,null,modules,urls,function () {
console.log('done');
});
3. В базе данных, обойти всех детей, внуков и т.д. некоторого родителя.
global.SynJS = global.SynJS || require('synjs');
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost',
user : 'tracker',
password : 'tracker123',
database : 'tracker'
});
function mysqlQueryWrapper(modules,context,query, params){
var res={};
modules.connection.query(query,params,function(err, rows, fields){
if(err) throw err;
res.rows = rows;
res.done = true;
SynJS.resume(context);
})
return res;
}
function getChildsWrapper(modules, context, doc_id, children) {
var res={};
SynJS.run(modules.getChilds,null,modules,doc_id, children, function (ret) {
res.result = ret;
res.done = true;
SynJS.resume(context);
});
return res;
}
function getChilds(modules, doc_id, children) {
var ret={};
console.log('processing getChilds:',doc_id,SynJS.states);
var docRec = modules.mysqlQueryWrapper(modules,_synjsContext,"select * from docs where id=?",[doc_id]);
SynJS.wait(docRec.done);
ret.curr = docRec.rows[0];
ret.childs = [];
var docLinks = modules.mysqlQueryWrapper(modules,_synjsContext,"select * from doc_links where doc_id=?",[doc_id]);
SynJS.wait(docLinks.done);
for(var i=0; docLinks.rows && i < docLinks.rows.length; i++) {
var currDocId = docLinks.rows[i].child_id;
if(currDocId) {
console.log('synjs run getChilds start');
var child = modules.getChildsWrapper(modules,_synjsContext,currDocId,children);
SynJS.wait(child.done);
children[child.result.curr.name] = child.result.curr.name;
}
}
return ret;
};
var modules = {
SynJS: SynJS,
mysqlQueryWrapper: mysqlQueryWrapper,
connection: connection,
getChilds: getChilds,
getChildsWrapper: getChildsWrapper,
};
var children={};
SynJS.run(getChilds,null,modules,12,children,function (ret) {
connection.end();
console.log('done',children);
});
На данный момент я использую SynJS для написания браузерных тестов, в которых требуется имитировать сложные пользовательские сценарии (кликнуть ”New”, заполнить форму, кликнуть ”Save”, подождать, проверить через API что записалось, и т. п.) — SynJS позволяет сократить код, и самое главное, повысить его понятность.
Надеюсь, кому-то он тоже окажется полезен до тех пор, пока не наступило светлое будущее с async/await.
> Проект на гитхабе
> NPM
P.S. Чуть не забыл, в SynJS имеется оператор SynJS.goto(). А почему бы и нет?
Комментарии (119)
alQlagin
08.01.2017 09:33SynJS.run(myFetches,null,modules,urls,function () { console.log('done'); });
Если после завершения нужно выполнить еще что-то с полученным результатом, то это будет выглядеть так?
SynJS.run(myFetches,null,modules,urls,function () { SynJS.run(myAfterFetches,null,modules,??result?? /*где бы его получить*/,function () { console.log('done'); }); });
или есть техника как избежать SynjsHell, простите за каламбур
amaksr
08.01.2017 09:54Если в myFetches есть return, то его результат будет параметром колбека:
function myFetches(modules, urls) { ... return 123; } SynJS.run(myFetches,null,modules,urls,function (res) { console.log(res); <-- напечатает 123 });
Можно вызывать вложенные SynJS.run, в этой части все как в обычном JavaScript. Ограничения касаются, в основном, функции, которая исполняется через SynJS.run,
В 3-м примере показано как SynJS.run вызывается рекурсивно чтобы обойти дерево.alQlagin
08.01.2017 10:57В 3-м примере показано как SynJS.run вызывается рекурсивно чтобы обойти дерево.
я имею ввиду после того как мы получили дерево и хотим в с ним что-то сделать. Например отфильтровать узлы.
Можно вызывать вложенные SynJS.run, в этой части все как в обычном JavaScript
ну то есть от callback hell мы никуда не ушли?
SynJS.run(myFetches,null,modules,urls,function (res) { // обработка ошибки 1? SynJS.run(filterTree, null,modules,res,function (res) { // обработка ошибки 2? SynJS.run(doSomethingWithFilteredTree, null,modules,res,function (res) { // обработка ошибки 3? console.log(res); }); }); });
@amaksr так?
amaksr
08.01.2017 11:24ну то есть от callback hell мы никуда не ушли?
Мы ушли от callback-hell только внутри функции, вызываемой через SynJS.run. Все остальные функции подчиняются тем же законам JavaScript, что и раньше. Точно так же в случае async/await мы должны объявить функцию через async, если мы собираемся в ней ждать коллбеки (ну и плюс еще сделать оболочки с Promises для функций с колбеками, которые мы собираемся вызывать).
Вообще этот момент мне более всего непонятен: почему нельзя было ввести в JavaScript оператор, который бы приостанавливал исполнение контекста без блокировки других контекстов, лет так 10 назад? Тогда никто и не знал бы сейчас про callback hell. Почему только недавно такая возможность появилась, но и то в виде генераторов? Выглядит так, что кто-то сильно ошибся с дизайном когда-то давно, поэтому мы сейчас и имеем все эти костыли.vintage
08.01.2017 12:12В NodeJS эта возможность появилась 6 лет назад.. А не стандартизировали это потому, что дизайном языка занимаются не грамотные архитекторы, а толпа леммингов.
vintage
08.01.2017 12:15Не хотите ли добавить SynJS в эту коллекцию асинхронных паттернов? https://github.com/nin-jin/async-js
amaksr
09.01.2017 07:42Добавить можно, но так как в предложенном тесте всего лишь одна асинхронная операция, а в функциях практически нет логики, то смысла это особого не имеет, и код только раздуется.
vintage
09.01.2017 08:11Что предложите добавить, чтобы это обрело смысл?
amaksr
09.01.2017 21:00SynJS лучше справляетс с задачами, где перемешаны колбеки, цикли, условия и рекурсии.
Но ваша задача натолкнула меня на мысль, что в хорошо бы добавить возможность приостанавливать не только операторы в некоторой функции, но и вычисление выражений. Тогда код, который сейчас в SynJS выглядит так:
var res1 = query("select 1");
SynJS.wait();
var res2 = query("select 2");
SynJS.wait();
var res = res1 + res2;
можно было бы сократить до
var a = query("select 1") + query("select 2");
Наверное попробую это реализовать…
brooth
08.01.2017 12:49+10Не пойму, почему немного не потерпеть пока async/await пойдет в масссы а пока пересидеть на babel?
valiorik
08.01.2017 15:16+1Потому что IE?
Все бизнесы, знаковые мне изнутри, поддердживают 2+ старых версий IE, для которых писать быстрый JS очень не просто.brooth
08.01.2017 16:13-1Я не фронтендщик, возможно чего-то не понимаю, но разве IE 2 может в ajax? Мы же тут про async говорим… Опять же, имеет ли такое значение скорость javascript-а, когда у нас тут асинхронный запрос на сервер?
yogurt1
08.01.2017 15:34+1Про async/await чистая ложь
Во-первых, transform-async-to-generator просто заменяет все await на yield и оборачивает функцию в вызов функции co (github.com/tj/co, можно свою реализацию подставить). Также есть asynctogen, с которым нет смысла тащить babel, если других фич не используете
Во-вторых, у вас включен regenerator, и поэтому код страшныйguyfawkes
08.01.2017 17:32Как без включения regenerator будет работать конвертация async/await?
SuperPaintman
10.01.2017 09:28+1regenerator преобразует генераторы в стейт-машину которая будет работать и на старых версиях V8, которые не поддерживают их.
А async/await может спокойно без него работать, если упомянутые выше генераторы поддерживаются движком (благо они уже более распространены)
guyfawkes
10.01.2017 13:53Я именно про случай преобразования для движков, не поддерживающих async/await
staticlab
10.01.2017 13:57+1Вы хотели сказать "не поддерживающих генераторы"?
guyfawkes
10.01.2017 14:40Не совсем понимаю вас. Смотрим на сводную таблицу, к примеру, здесь:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
Если хром будет не 55, а 54 версии, он, судя по всему, не будет поддерживать async. Как поддержка генераторов ему поможет?staticlab
10.01.2017 14:51+2transform-async-to-generator просто заменяет все await на yield и оборачивает функцию в вызов функции co (github.com/tj/co, можно свою реализацию подставить)
Суть в том, что async-await вначале преобразуется бабелем в генераторы, а уже затем генераторы (ES6) транспилируются в стейт-машину для регенератора. Таким образом, если целевые браузеры поддерживают генераторы нативно, то и регенератор не нужен.
Вам это объясняли выше, но вы задали такой вопрос, как будто бы имели в виду транпиляцию async-await для движков, не поддерживающих генераторы, почему я и переспросил.
apelsyn
08.01.2017 16:15+2Еще про генераторы забыли. Они поддерживаться уже давно.
amaksr
08.01.2017 20:36-1Генераторы поддерживаются не везде. Они необходимы чтобы нативно приостанавливать выполнение функции и реализовать ожидание чего-то (возможность приостанавливаться и ждать как раз и позволиляет избавится от колбеков), но в IE например их нет. Поэтому Babel фактически создает свою State machine, парсит код функции и исполняет ее операторы сам, без вызова этой функции напрямую.
staticlab
09.01.2017 21:40А вы не проверяли, что быстрее: нативные генераторы, генераторы через regenerator или ваш велосипед?
amaksr
09.01.2017 22:30Я тестировал вот этой програмкой:
спойлерglobal.SynJS = require("synjs"); function publishLevel(modules) { var levels=[]; var start = new Date().getTime(); for(var i=0; i<100000; i++) { var user = modules.getUser(i); var can_create = modules.canCreate(user); if(!can_create) var level = modules.saveLevel(user, null); levels.push(level); } return new Date().getTime()-start; } function getUser(user_id) { return { id: user_id, nickname: 'tlhunter' }; } function canCreate(user) { return user.id === 12; } function saveLevel(user, data) { return { id: 100, owner: user.nickname, data: data }; } var modules = { getUser: getUser, canCreate: canCreate, saveLevel: saveLevel }; SynJS.run(publishLevel,null,modules,function(ret){ console.log('ret=',ret); })
KlonD90
08.01.2017 16:24Ко всему прочему хотел бы докинуть что у вас же вышли старые добрые fiber'ы и они уже давно есть в NodeJS как одно из решений но от них отказываются. Как-то генераторы надежнее выглядят и сводятся к тому же.
3al
08.01.2017 16:33Но это же не решение. Суть callback hell — в том, что с ним сложно обрабатывать ошибки читаемым способом. Этот велосипед не улучшает вообще ничего.
feverqwe
08.01.2017 16:39+1Что написали велосипед — хорошо, возможно что то поняли. Но не надо это использовать нигде, хотя бы потому, что
оно парсит код. От этого мало того, что отваливается вся оптимизация, которые делает браузер, но и уж точно вы не учли все возможные виды написания кода, который уж точно не уместится в 34kb.
Просто используйте Promise, и не нужно мучить ни себя не других. Если кто то внезапно столкнется с проблемами, которые вызывает ваш велосипед — цена переписывания кода с синхронного с костылями на асинхронный будет слишком высока, проще сразу писать нормально.
Да и полифил promise не минифицированный занимает всего 8kb, а уменьшенный 3kb.amaksr
08.01.2017 20:25-1Распарсенные операторы в SynJS компилируются в функции через eval (насколько я знаю Node тоже загружает файлы через eval). Поэтому все оптимизации, происходящие в eval никуда не отвалятся. К тому же делается это только 1 раз при первом вызове функции.
Promise не поможет мне написать понятный код, в котором перемешаны функции с колбеками, циклы, условия и рекурсия. Написать то конечно можно, но достаточно взглянуть на StackOverflow — там каждый день идут десятки вопросов как реализовать тот или иной алгоритм с Promises, и предлагаемые решения не выглядят интуитивно понятно.
titov_andrei
08.01.2017 17:03Заказчику разве не всё равно, какой дрелью ему отверстие в стене будут делать?
feverqwe
08.01.2017 17:12Ну хоть что бы себя пожалеть, не рефакторить лишний раз, да и все равно думаю просветление придёт рано или поздно к автору и он от этого откажется, а поддерживать потом что то придется с этим. Самое то страшное, вот встретили вы багу где то, например в парсере, починили, а кто даст гарантию что где то что то не отвалилось?
rumkin
08.01.2017 19:11Велосипед не засчитан, так как не работает в ряде случаев. Вариант с парсингом затирает контекст исполнения, т.о. я не могу быть уверен в том, что функция полученная откуда-то будет работать предсказуемым образом.
Rulexec
08.01.2017 20:02Выглядит так, будто бы оно просто распарсивает функцию и делает из неё генератор.
Почему они с тем же успехом не могли распарсивать функцию и впиливать поддержку await/async? Если это так хочется делать в рантайме, а не транспилировать.
token
08.01.2017 20:30Парсер классный, аж волосы зашевелились )
amaksr
08.01.2017 23:42Парсер пролучился довольно простой так как он парсит функцию, которая уже откомпилирована движком, а значит имеет корректный синтсксис. Если бы нужно было парсить произвольный текст, то парсер бы раздулся во много раз, и в 35кб он бы не уложился.
token
09.01.2017 00:48Простой? Посмотрите как выглядит нормально и понятно написанный парсер рекурсивного спуска: https://github.com/angrycoding/javascript-parser/blob/master/src/parser/Parser.js
token
09.01.2017 00:53Кстати, если хотите реально сделать что то полезное (в первую очередь для себя), то вместо костылей напишите ограниченный сабсет джаваскрипта, с поддержкой всего того что Вам нужно и компилятором в обычный JS.
token
08.01.2017 20:33А так да, велосипед тот ещё, во — первых рантайм, а во — вторых потеря контекста.
faiwer
08.01.2017 21:15Какое уродливое не-решение никаких проблем. Не знаю зачем вы его придумали, и для чего это может пригодиться. Возьмите уж тогда хотя бы promise-hell, всяко менее вырвиглазнее, да и без runtime парсинга js-а.
Но вот одну интересную мысль из топика я таки выцепил. Вы упомянули о том, что babel-polyfil весит под 100 KiB. Я с возмущением и словами "да не может быть, какая ерунда" полез смотреть и опешил. 97 KiB. Гхм. Грусть-печаль меня охватила.
Jack38b
08.01.2017 21:19По-моему здесь есть неплохое решение:
Заголовок спойлераfunction processInOrder(arr, cb){
if( arr.length > 0 ){
arr[0].then(function(result){
cb(result);
processInOrder(arr.slice(1), cb);
});
}
}
var deferreds = [];
for(var i=0; i<4; i++){
deferreds.push( asyncRequest(i) );
}
processInOrder(deferreds, display);Jack38b
08.01.2017 21:24Упс. Ссылка потерялась:
http://stackoverflow.com/questions/34186336/how-to-process-the-results-of-a-series-of-ajax-requests-sequentiallyvintage
08.01.2017 21:32Сравните с этим:
let action = new Atom( 'action' , ()=> { let responses = [] for( let i = 0 ; i < 4 ; ++i ) responses.push( asyncRequest( i ) ) responses.forEach( display ) } ) action.value()
Jack38b
08.01.2017 21:50Врядле я бы стал это использовать. Мой код элементарно не пройдет pull request если я начну 3-rd party библиотеки/фреймфорки использвоать. Но выглядит конечно красиво. Цикл вместо рекурсии (оч. хорошо — легче оргазизовать scalability — хотя это чаще на бэкенде нужно но все же). Кстати, а эти все Atom — это не есть то же самое, что и observer pattern? CanJS такие вещи по-моему делает, и приводит это к тому, что садится performance.
vintage
08.01.2017 21:56Не очень верится, что вы не используете ни одной сторонней библиотеки.
Jack38b
08.01.2017 22:02Конечно используем. Но в них нет Atom
vintage
08.01.2017 22:24Что же мешает его добавить?
Jack38b
08.01.2017 22:26Выше я вопрос задал, повторю: «Atom — это не есть то же самое, что и observer pattern? CanJS такие вещи по-моему делает (там есть Observables), и приводит это к тому, что садится performance.»
vintage
08.01.2017 23:10Это скорее computed паттерн. Есть он много где, но зачастую реализация не эффективна. Оцените масштаб трагедии.
Jack38b
08.01.2017 23:23«это » — это вы про что? Про Атом или про CanJS? В CanJS «Observables are the subjects in the observer pattern.». А вот что такое Атом я не знаю.
В любом случае, я даже синтаксис вашего кода не понимаю, чтобы оценить всю его красоту. Я на javascript работаю всего год, и не знаю что такое, скажем, "()=>" (чем то похоже на C# лямбду). В целом мне кажется, что call back hell проблема состоит из двух частей — если работаешь с legacy code в большой кампании, и там много всего уже написано. А если разрабатываешь с нуля, то свой собственный код можно писать аккуратно, хотя конечно уйдет полгода пока привыкнешь к всем этим call back и deferred и Promise. Хотя, конечно, вынужден признать, что разработка на JS действительно challenging.vintage
08.01.2017 23:42Про атомы. Тут про них подробнее. Впрочем, CanJS тоже поддерживает "computed".
Да,
()=>
— это та же лямбда. То же что иfunction(){ ... }
, только сохраняет this.
Jack38b
08.01.2017 23:50Про лямбду заинтриговали. Как это использовать в своем коде? Что нужно добавить, чтобы использовать эту конструкцию?
vintage
09.01.2017 00:07Нужен любой современный браузер, нода или какой-нибудь транспилятор. Лично я пользуюсь тайпскриптом.
Jack38b
09.01.2017 00:17Node у нас стоит, Chrome современный, а вот тайпскрипт в нашем environment не используется. Так что не судьба. Но красиво выглядит :)
vintage
09.01.2017 00:23+1Я же написал или.
Jack38b
09.01.2017 00:30Ясно. Попробую вставить ваш код снипет в свой проект… Удивлюсь если сработает, но кто знает :) Спасибо
Jack38b
09.01.2017 02:32А вас можно пользуясь случаем спросить — что предпочтительнее использовать при работе с Git? Я использую с командной строки или из sublime, но мне это кажется крайне неэффективным (я люблю конечно, командную строку, но не merge же в ней делать!) к тому же, часто приходится искать чей то commit и приходится для этого использовать комбинацию tools (Visual Studio + Sublime + command line). Заранее благодарен!
vintage
09.01.2017 08:13TortoiseGit, например.
Jack38b
13.01.2017 18:49Спасибо большое. Есть у меня еще один вопрос, если у вас есть время ответить буду жутко признателен.
Я ищу вот какую вещь: имея удаленную git master branch, я хотел бы запускать какой нибудь скрипт (предпочтительно перл + git command) чтобы на выходе получить текстовый файл с набором diffs по каждому commit определенного пользователя.
В свое время я смог достаточно быстро сделать нечто подобное для ClearCase (это как Git только от айбиэм) даже не будучи знакомым с ClearCase вообще. Но когда попробовал сделать тоже самое в git у меня не получилось, хотя я потратил существенно больше времени. Если у вас есть ссылка на нечто подобное или совет — буду крайне признателен.
Большое спасибо!vintage
13.01.2017 20:22Jack38b
14.01.2017 18:44Возможно я злоупотребляю, но есть еще вопрос :)
Я чаще работаю с чужим JS кодом. Мне часто приходится находить код- обработчик события. Я написал собственный скрипт, который в консоли выдает всех подписчиков на событие (jquery) по елементу, на который я ткнул мышкой ($0 в chrome), потом я по одному их всех перебираю и в среднем у меня уходит примерно час чтобы найти call back обработчик, скажем, нажатия мыши. Причем мозги после этого выпарены.
Вопрос вот в чем — если способ находить это как то быстрее? И еще, я подозреваю, что опытные разработчики не занимаются поиском обработчиков перебором колбэков которые зарегистрированы на event. Может воркфлоу должен быть каким то иным? Я просто действую так, как привык — мой предыдущий опыт разработки в GUI был для деск топ где такого рода вещи делаются за несколько секунд, даже не минут. Я догадываюсь что что то я делаю не так, но не могу понять что именно. Буду признателен за совет :)vintage
14.01.2017 20:02Для вопросов есть специальный ресурс: https://toster.ru
А так, в Chrome Dev Tools можно смотреть обработчики повешенные на выделенный элемент и всех его предков.Jack38b
14.01.2017 20:22Про Dev Tools event listeners я знал, но я думаю, это не то же самое, что обратотчики jQuery (я могу чего то не понимать).Про ресурс спасибо, вопросы буду задавать там.
vintage
14.01.2017 22:42Там через скоупы можно добраться до нужной функции.
Jack38b
14.01.2017 23:06Спасибо большое. Но опять интересно. Какими scope? Может ссылку кините в виде исключения перед тем как я пойду задавать вопрос на тостере?
До этого я вот этим пытался пользоваться (то о чем я выше говорил):
http://stackoverflow.com/questions/570960/how-to-debug-javascript-jquery-event-bindings-with-firebug-or-similar-tool
с некоторыми модификациями. Получается не слишком хорошо — вылетает обычно несколько десятков обработчиков, в итоге приходится ставить брейкпоинт внутри jquery и уже оттуда я нахожу то, что некое CanJS-овское подобие Object.defineProperty. Devtools event listeners вообще ничего мне не дали в принципе. В итоге сейчас я просто сижу в отладчике чтобы найти то, что мне надо или пытаюсь искать по комитам в git если кто то делал что то подобное и пытаюсь оттуда посмотреть.
Jack38b
13.01.2017 20:28Т.е. я понимаю, что надо слать комманду
git --no-pager diff <commit^> в цикле, но проблема с merges — я хочу чтобы без них было. А я их получаю с помощью
git --no-pager log и приходит много мусора.
amaksr
08.01.2017 21:52Это решение для очень простой проблемы, так как все итерации однотипные, и не связаны никакой логикой. Такую проблему можно решать легко и через рекурсию, и через Promises, и через async.js. Все становится гораздо сложнее если вам надо делать циклы или условия в каждой итерации, и по их результатам вызывать другие асинхронные функции и ждать колбеков от них
Jack38b
08.01.2017 22:09Верно, но сравнивать нужно сравнимые вещи. В приведенном в статье сниппете решается та же проблема что и в приведенном мною. Если будут другие — более сложные — то и решения будут выглядеть иначе.
На самом деле мне встречались типовые design patterns на javascript с использованием promises которые решают многие типовые проблемы — последовательная обработка только один из них.
Ну и потом, при разработке GUI на javascript последовательная обработка не является слишком частым делом — насколько я понимаю предлагаемое решение нацелено именно на решение этой проблемы.
Мне кажется разработчику вроде меня было бы трудно убедить коллег в использовании того, что вы создали для решение проблемы callback hell.
velvetcat
08.01.2017 21:40Коллеги, подскажите ненастоящему JS-нику, а способ решения проблемы с callback hell с помощью отказа от инлайнинга функций — годный/в стиле JS?
Бонусом к этому способу идет соблюдение SRP, нормальное юнит-тестирование и все такое.
RubaXa
08.01.2017 22:03+1На данный момент я использую SynJS для написания браузерных тестов
Т.е. там, где даже мегабайты «лишнего» кода некритичны, да ещё и не продакшен это.
Ну и даже не заглядывая в исходники ставлю на полную неработоспособность при использовании любого обфускатора.
P.S. А ведь всё могло быть по другомуБудущее, судя по всему, за async/await, но пока это будущее не наступило, и многие движки эту возможность не поддерживают.
Кроме того, Babel тянет за собой около 100 кб минифицированного кода «babel-polyfill», а сконвертированный код работает медленноПосмотрев на все это, я решил написать
свой велосипед — SynJSсвою урезанную версию реализацииasync/await
на голой Эсприме...amaksr
08.01.2017 23:56Инструмент новый, пока обкатывается в тестах, вроде показывает себя хорошо, а значит будет и в продакшене.
Насчет обфускатора вы скорее всего правы, но иной раз код с Promises, логикой с рекурсией, циклами и колбеками выглядит так, что никакого обфускатора и не надо.RubaXa
09.01.2017 00:27Ой, ну не надо, продуманная декомпозиция решат всё.
Забыл добавить: Но, обычно, причина callback/promise hell, банальная лень.
amaksr
09.01.2017 02:18Да, банальная лень, только эта лень разработчиков самого языка JavaScript, которые в течение долгих лет не давали возможность приостанавливать функции нативно и без блокировки, что собственно и породило саму проблему callback hell.
TheShock
09.01.2017 03:06+1Мы запретили на одном из крупнейших проектов брать анонимные функции вообще (трудно утечки с ними контролировать) и не было никакого колбек-хела, ибо приходилось правильно декомпозировать
RubaXa
09.01.2017 00:32+1Блин, хабр, что ты сделал.
Насчет обфускатора вы скорее всего правы, но иной раз код с Promises, логикой с рекурсией, циклами и колбеками выглядит так, что никакого обфускатора и не надо.
Ой, ну не надо, продуманная декомпозиция решат всё.
Вот вы пишите про какие-то там задачи, в которых Callback/Promise страшны, но в статье их не приводите, а показываете какие-то элементарные вещи.
Потом идет пример на async/await, дальше пугаем
babel-polyfill
(который содержит поддержку генераторов, итераторов и много чего), говорим что спасение есть, этоSynJS
и… не приводим примера той же задачи на неё, а показываем какую-то мутьmyTestFunction1
и csetTimeout
.
P.S. Обфускатор (он же и минификатор), сейчас используют все по умолчанию.
AndreyRubankov
11.01.2017 12:22Почему в каждой статье про «callback hell» всегда приводят пример самого тупого подхода к написанию кода?
fetch(“list_of_urls”, function(array_of_urls){ for(var i=0; array_of_urls.length; i++) { fetch(array_of_urls[i], function(profile){ fetch(profile.imageUrl, function(image){ ... }); }); } });
Почему в качестве решения люди предлагают использовать сторонние библиотеки, и даже хотят вводить async/await, которые по факту не решают проблему, а лишь маскируют ее?
Разве не легче просто писать хороший код, который будет сам за себя говорить, что он делает?
fetch(“list_of_urls”, _loadAllFetchedUrls); function _loadAllFetchedUrls(array_of_urls) { for(var i=0; array_of_urls.length; i++) { fetch(array_of_urls[i], _loadProfileImage); } } function _loadProfileImage(profile){ fetch(profile.imageUrl, _showProfileImage); }
В данном случае все функции будут поддаваться оптимизации со стороны js движка, не будет генерироваться лишний мусор, сам код становится читабельным — имя функции сразу говорит, что будет сделано после загрузки.
Есть недостаток в том, что всю цепочку не видно, но это еще ни разу не было критичной проблемой, при этом любая более-менее хорошая IDE покажет всю цепочку вызовов до конкретного callback'а.
По-моему такой подход решает проблему «callback hell» без каких-либо сторонних библиотек и без нововведений на уровне языка (async/await).
Я, конечно, могу ошибаться и если это так, буду рад услышать в чем именно я ошибся.faiwer
11.01.2017 14:02+1По-моему такой подход решает проблему «callback hell» без каких-либо сторонних библиотек и без нововведений на уровне языка (async/await).
Вы превращаете вложенную-лапшу в вертикальную-лапшу. Никаких проблем вы этим НЕ решаете. Любой нетривиальный случай вырождается при таком подходе в груду методов с дурацкими названиями, с кучей аргументов и многочисленной копипастой.
faiwer
11.01.2017 14:13Пример из живого кодаasync openChild(id) { try { const cached = id in this.state.map; let widget = cached && this.state.map[id]; if(!cached) { this.updateHabData(this.activeId, { habLoading: true }); widget = await this.load(id); this.updateHabData(this.activeId, { habLoading: false }); } await this.updateHabData(id, { id, title: widget.title, widget, loading: false, habLoading: false, failed: false }); this.setActiveId(widget.id); this.navigator.pushPage(this.createRoute(widget)); if(cached) { const widget = await this.load(id); // обновляем this.updateHabData(id, { widget }); } } catch(err) { this.updateHabData(this.activeId, { habLoading: false }); this.notifyError(err, this.l('failLoad')); } }
AndreyRubankov
11.01.2017 15:37Спасибо за пример кода, это, наверно, первый адекватный пример, который я увидел, где использование async/await действительно упрощает понимание кода.
Обычно приводят тривиальные и глупые примеры, которые даже под разряд callback hell не подходят.
RubaXa
11.01.2017 17:13Это какой-то, кхм, «странный» метод, где-то просто
this.updateHabData
, где-тоawait this.updateHabData
, wtf?AndreyRubankov
11.01.2017 18:52Там где с await будет последовательно код идти, там где без —
асинхроннобесконтрольно:
this.updateHabData(this.activeId, { habLoading: true }); widget = await this.load(id);
— порядок выполнения случайный.
В конкретно этом коде проблем быть не должно, но в целом, это может породить гонку выполнения! (особенно на запросах к серверу) и соответственно вылиться в эпический факап. Но кого это волнует?!RubaXa
11.01.2017 18:57Код с душком, будем честными (проверка на
cached
вызывает недоумение, как и в целом работа сupdateHabData
?!).faiwer
11.01.2017 19:56А в чём собственно проблема? :) Расстановка await перед всеми вызовами updateHabData по сути ничего не поменяет. Это так, экономия на спичках (и муках отладки регенератора в случае чего), наверное избыточная, но никакой трагедии уж точно. Не думал что она кого-то смутит.
А что не так с проверкой на cached? Она тут нужна ввиду того, что возможны два сценария:
- Данные берутся из кеша и отображаются моментально. В этом случае показываем их сразу, но втихую догружаем обновление
- Данных в кеше нет, и мы вынуждены показывать спиннер, дожидаясь загрузки.
Отдельного упоминания заслуживает onsen-ий navigator.pushPage, которые занимается разного рода анимациями перелистывания экранов (ради чего и приходится городить столько кода).
RubaXa
11.01.2017 22:47Расстановка
await
в произвольно порядке говорит только о том, что над кодом особо не думали и так сойдёт.
С
cached
тоже самое, методopenChild
умеет работать с кешем, загружать данные, показывать спиннеры, что-то обновлять (но при этом сам кеш не кладёт), наверно и кофё варить ;] Всё перемешалось в этом методе.faiwer
12.01.2017 06:59Повторяю, await расставлены не в произвольном порядке, а в таком, что await-ятся только те операции, дождаться окончания которых критично (тут нет никаких race conditions). Повторюсь, внутри .setState от react-компоненты. Часто вы в своём коде передаёте туда callback? Думаю крайне редко, однако такие ситуации могут быть полезными. В данном случае одна такая в коде показана, т.к. state обязательно должен обновиться до того, как будет вызван onsen.navigator.pushState (иначе он упадёт не найдя нужных для render-а данных).
Всё перемешалось в этом методе.
Соглашусь только с этим. Но, увы, некоторые UI задачи требуют заморочек. А cache обновляется внутри load-а.
vintage
12.01.2017 09:28+1await выполняет не только функцию "приостановить в ожидании успешного выполнения асинхронной функции", но и "кинуть исключение в случае безуспешного выполнения асинхронной функции". В случае ошибки в updateHabData вы не сможете её никак обработать. Собственно это — основная проблема асинхронного кода — нужно очень внимательно следить за обработкой ошибок при каждом асинхронном вызове и очень легко ошибку проигнорировать, оставив приложение в поломанном состоянии.
faiwer
12.01.2017 09:46А вот с этим соглашусь. Серьёзный аргумент. Что с await, что с promise-ми, да даже callback-ми очень просто пустить некоторые ошибки на самотёк. В nodejs для этого даже костыль в виде uncaughtException воткнули.
faiwer
11.01.2017 19:51Я просто расставил await только в тех местах, где критично дождаться окончания его выполнения (там внутри react-ий setState, который может быть обработан асинхронно). Где не критично пустил его на самотёк.
alQlagin
Судя по ридми на github в библиотеке отсутствует обработка ошибок
Тот же
костыльPromise отлично с этим справляется.Кстати, так вами любимый async/await тоже использует Promise
подробнее можно например тут посмотреть https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
amaksr
try...catch недоступно только в теле самой вызываемой функции, которая запускается через SynJS.run (myTestFunction1 в статье). В любых других функциях, в том числе вызываемых из myTestFunction1 try...catch доступен.
да, но async/await стал возможен не благодаря Promises, а благодаря генераторам, которые нативно позволяют останавливать/возобновлять выполнение контекста в движке. Чтобы получить Promises достаточно подключить небольшой полифил, а вот чтобы получить остановку/возобновление контекста исполнения в ES2015 в Babel фактически пришлось создать State machine, парсить и эмулировать исполнение операторов функции примерно так же, как это делает SynJS.staticlab
Но ведь это в ES2015, в то время как async-await уже доступны нативно в Chrome 55, ожидаются в FF 52, находятся в разработке в Edge, Safari и Node.js. Суть в том, что async-await стандартизованы, и они скоро появятся во всех браузерах, а SynJS придётся тащить за собой.
Finom
На ноде уже есть async/await.
staticlab
Он пока что под --harmony и не рекомендуется, поскольку там есть баг с утечкой памяти. Собственно, поэтому я и отнёс ноду в раздел «в разработке».
amaksr
IE ближайшие пару лет никуда не денется, особенно из западных компаний, у которых обычно есть policies не обновлять ПО до самых последних версий.
amaksr
Интересно, как любое упоминание IE моментально набирает минусы. Но вот я сейчас работаю с 2-мя западными компаниями, которые сидят на Windows 7 и IE 9, и обновлений на Edge даже и не планируют пока. Они совместимость со своим старьем всегда ставят в условия ТЗ. Не отказывать же им…
taliban
Отказывать, внезапно.
http://bluebirdjs.com/ зацените штуку, работает начиная с ие7 весит меньше чем ваша библиотека, полностью совместима с promise, проверена временем, и не имеет недостатков которые у вас пока еще есть.
kk86
Как я вас понимаю!
vintage
плюс похоже будет проблема с замкнутыми переменными вне SynJS.run
amaksr
Функция, вызываемая через SynJS.run, ничего не будет знать о своем окружении, так как она не вызывается JS-движком напрямую:
К ней надо относится так, как если бы она была определена где-то в другом модуле, и передавать необходимые переменные через параметры, obj (this внутри функции), или global.