8 января в 08:20

Еще один велосипед для борьбы с callback hell в JavaScript



Считается, что мир 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 это костыль, потому что

    1. цепочки вызывают функции только в одном заданном порядке,
    2. если порядок может менятся в соответсвии с какой-то логикой, по-прежнему приходится дробить логику в колбеках на отдельные функции,
    3. для кодирования логики между функциями по-прежнему приходится что-то изобретать, вместо того, чтобы просто пользоваться стандартными операторами if, for, while и т.п.
    4. логика с 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:

Cпрятано в спойлер
"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(). А почему бы и нет?
@amaksr
карма
24,0
рейтинг 0,4
Похожие публикации
Самое читаемое Разработка

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

  • +7

    Судя по ридми на github в библиотеке отсутствует обработка ошибок


    Following operators are not yet supported:
    • const
    • let
    • for… of
    • try… catch
      а значит вся её ценность равна нулю.

    Тот же костыльPromise отлично с этим справляется.


    Кстати, так вами любимый async/await тоже использует Promise


    When async function is called, it returns a promise. When the async function returns a value, the promise will be resolved with the returned value. When the async function throws an exception or some value, the promise will be rejected with the thrown value.

    Async function can contain await expression, that pauses the execution of the async function and waits for the passed promise's resolution, and resumes the async function's execution and returns the resolved value.

    подробнее можно например тут посмотреть https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

    • –1
      try...catch недоступно только в теле самой вызываемой функции, которая запускается через SynJS.run (myTestFunction1 в статье). В любых других функциях, в том числе вызываемых из myTestFunction1 try...catch доступен.
      Кстати, так вами любимый async/await тоже использует Promise
      да, но async/await стал возможен не благодаря Promises, а благодаря генераторам, которые нативно позволяют останавливать/возобновлять выполнение контекста в движке. Чтобы получить Promises достаточно подключить небольшой полифил, а вот чтобы получить остановку/возобновление контекста исполнения в ES2015 в Babel фактически пришлось создать State machine, парсить и эмулировать исполнение операторов функции примерно так же, как это делает SynJS.
      • +3

        Но ведь это в ES2015, в то время как async-await уже доступны нативно в Chrome 55, ожидаются в FF 52, находятся в разработке в Edge, Safari и Node.js. Суть в том, что async-await стандартизованы, и они скоро появятся во всех браузерах, а SynJS придётся тащить за собой.

        • +1

          На ноде уже есть async/await.

          • 0

            Он пока что под --harmony и не рекомендуется, поскольку там есть баг с утечкой памяти. Собственно, поэтому я и отнёс ноду в раздел «в разработке».

        • –3
          IE ближайшие пару лет никуда не денется, особенно из западных компаний, у которых обычно есть policies не обновлять ПО до самых последних версий.
          • –1
            Интересно, как любое упоминание IE моментально набирает минусы. Но вот я сейчас работаю с 2-мя западными компаниями, которые сидят на Windows 7 и IE 9, и обновлений на Edge даже и не планируют пока. Они совместимость со своим старьем всегда ставят в условия ТЗ. Не отказывать же им…
            • +1
              Отказывать, внезапно.
              http://bluebirdjs.com/ зацените штуку, работает начиная с ие7 весит меньше чем ваша библиотека, полностью совместима с promise, проверена временем, и не имеет недостатков которые у вас пока еще есть.
            • 0

              Как я вас понимаю!

    • +3

      плюс похоже будет проблема с замкнутыми переменными вне SynJS.run

      • 0
        Функция, вызываемая через SynJS.run, ничего не будет знать о своем окружении, так как она не вызывается JS-движком напрямую:
        var i=123;
        function myTestFunction1() {
            console.log(i); <--- i будет undefined
        }
        SynJS.run(myTestFunction1,obj, function () {
            console.log('done all');
        });
        

        К ней надо относится так, как если бы она была определена где-то в другом модуле, и передавать необходимые переменные через параметры, obj (this внутри функции), или global.
  • 0
    del
  • 0
    SynJS.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, простите за каламбур

    • 0
      Если в 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 вызывается рекурсивно чтобы обойти дерево.
      • 0
        В 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 так?

        • 0
          ну то есть от callback hell мы никуда не ушли?

          Мы ушли от callback-hell только внутри функции, вызываемой через SynJS.run. Все остальные функции подчиняются тем же законам JavaScript, что и раньше. Точно так же в случае async/await мы должны объявить функцию через async, если мы собираемся в ней ждать коллбеки (ну и плюс еще сделать оболочки с Promises для функций с колбеками, которые мы собираемся вызывать).

          Вообще этот момент мне более всего непонятен: почему нельзя было ввести в JavaScript оператор, который бы приостанавливал исполнение контекста без блокировки других контекстов, лет так 10 назад? Тогда никто и не знал бы сейчас про callback hell. Почему только недавно такая возможность появилась, но и то в виде генераторов? Выглядит так, что кто-то сильно ошибся с дизайном когда-то давно, поэтому мы сейчас и имеем все эти костыли.
  • 0

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

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

        Что предложите добавить, чтобы это обрело смысл?

        • 0
          SynJS лучше справляетс с задачами, где перемешаны колбеки, цикли, условия и рекурсии.
          Но ваша задача натолкнула меня на мысль, что в хорошо бы добавить возможность приостанавливать не только операторы в некоторой функции, но и вычисление выражений. Тогда код, который сейчас в 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");

          Наверное попробую это реализовать…
  • +10

    Не пойму, почему немного не потерпеть пока async/await пойдет в масссы а пока пересидеть на babel?

    • +1
      Потому что IE?
      Все бизнесы, знаковые мне изнутри, поддердживают 2+ старых версий IE, для которых писать быстрый JS очень не просто.
      • –1

        Я не фронтендщик, возможно чего-то не понимаю, но разве IE 2 может в ajax? Мы же тут про async говорим… Опять же, имеет ли такое значение скорость javascript-а, когда у нас тут асинхронный запрос на сервер?

        • +1

          Мне кажется, имелось в виду "не менее двух предпоследних версий", а не археология

          • 0

            Да, точно. Мне аж больно за коллег стало.

  • +1

    Про async/await чистая ложь
    Во-первых, transform-async-to-generator просто заменяет все await на yield и оборачивает функцию в вызов функции co (github.com/tj/co, можно свою реализацию подставить). Также есть asynctogen, с которым нет смысла тащить babel, если других фич не используете
    Во-вторых, у вас включен regenerator, и поэтому код страшный

    • 0
      Как без включения regenerator будет работать конвертация async/await?
      • +1

        regenerator преобразует генераторы в стейт-машину которая будет работать и на старых версиях V8, которые не поддерживают их.


        А async/await может спокойно без него работать, если упомянутые выше генераторы поддерживаются движком (благо они уже более распространены)

        • 0
          Я именно про случай преобразования для движков, не поддерживающих async/await
          • +1

            Вы хотели сказать "не поддерживающих генераторы"?

            • 0
              Не совсем понимаю вас. Смотрим на сводную таблицу, к примеру, здесь:
              https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

              Если хром будет не 55, а 54 версии, он, судя по всему, не будет поддерживать async. Как поддержка генераторов ему поможет?
              • +2
                transform-async-to-generator просто заменяет все await на yield и оборачивает функцию в вызов функции co (github.com/tj/co, можно свою реализацию подставить)

                Суть в том, что async-await вначале преобразуется бабелем в генераторы, а уже затем генераторы (ES6) транспилируются в стейт-машину для регенератора. Таким образом, если целевые браузеры поддерживают генераторы нативно, то и регенератор не нужен.


                Вам это объясняли выше, но вы задали такой вопрос, как будто бы имели в виду транпиляцию async-await для движков, не поддерживающих генераторы, почему я и переспросил.

                • 0
                  Мы оба недопоняли друг друга :) Спасибо за пояснения.
  • +2

    Еще про генераторы забыли. Они поддерживаться уже давно.

    • –1
      Генераторы поддерживаются не везде. Они необходимы чтобы нативно приостанавливать выполнение функции и реализовать ожидание чего-то (возможность приостанавливаться и ждать как раз и позволиляет избавится от колбеков), но в IE например их нет. Поэтому Babel фактически создает свою State machine, парсит код функции и исполняет ее операторы сам, без вызова этой функции напрямую.
      • 0

        А вы не проверяли, что быстрее: нативные генераторы, генераторы через regenerator или ваш велосипед?

        • 0
          Я тестировал вот этой програмкой:
          спойлер
          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);
          })
          
          



          В SynJS она завершилась за 87мс. Эквивалентный код с одним async, 3-мя await-ами, промисами (но без setTimeout-ов и других длительных функций) после Babel-я работал около 3 с.

          Нативные генераторы не проверял.
          • 0

            Зачем в этой абсолютно синхронной программе вообще использовать SynJS?

            • 0
              Все медленные функции были убраны специально для оценки быстродействия самого SynJS
              • +1

                В результате SynJS не делает ровным счётом ничего.

                • 0
                  Ну да. А сгенерированный Babel-ем код тоже не делает ничего, но только медленно.
                  • +1

                    Нет, он много чего делает. Пусть и впустую. Зачем вы вставляете async и await если функции синхронные?

          • +1

            "Нативные" промисы очень медленные. Надо брать Bluebird

  • 0

    Ко всему прочему хотел бы докинуть что у вас же вышли старые добрые fiber'ы и они уже давно есть в NodeJS как одно из решений но от них отказываются. Как-то генераторы надежнее выглядят и сводятся к тому же.

  • 0
    Но это же не решение. Суть callback hell — в том, что с ним сложно обрабатывать ошибки читаемым способом. Этот велосипед не улучшает вообще ничего.
  • +1
    Что написали велосипед — хорошо, возможно что то поняли. Но не надо это использовать нигде, хотя бы потому, что
    оно парсит код. От этого мало того, что отваливается вся оптимизация, которые делает браузер, но и уж точно вы не учли все возможные виды написания кода, который уж точно не уместится в 34kb.
    Просто используйте Promise, и не нужно мучить ни себя не других. Если кто то внезапно столкнется с проблемами, которые вызывает ваш велосипед — цена переписывания кода с синхронного с костылями на асинхронный будет слишком высока, проще сразу писать нормально.

    Да и полифил promise не минифицированный занимает всего 8kb, а уменьшенный 3kb.
    • –1
      Распарсенные операторы в SynJS компилируются в функции через eval (насколько я знаю Node тоже загружает файлы через eval). Поэтому все оптимизации, происходящие в eval никуда не отвалятся. К тому же делается это только 1 раз при первом вызове функции.

      Promise не поможет мне написать понятный код, в котором перемешаны функции с колбеками, циклы, условия и рекурсия. Написать то конечно можно, но достаточно взглянуть на StackOverflow — там каждый день идут десятки вопросов как реализовать тот или иной алгоритм с Promises, и предлагаемые решения не выглядят интуитивно понятно.
  • 0
    Заказчику разве не всё равно, какой дрелью ему отверстие в стене будут делать?
    • 0
      Ну хоть что бы себя пожалеть, не рефакторить лишний раз, да и все равно думаю просветление придёт рано или поздно к автору и он от этого откажется, а поддерживать потом что то придется с этим. Самое то страшное, вот встретили вы багу где то, например в парсере, починили, а кто даст гарантию что где то что то не отвалилось?
  • +1

    Редкий случай когда тезисы статьи не вызывают у меня отторжения.

  • 0

    Велосипед не засчитан, так как не работает в ряде случаев. Вариант с парсингом затирает контекст исполнения, т.о. я не могу быть уверен в том, что функция полученная откуда-то будет работать предсказуемым образом.

  • 0
    Выглядит так, будто бы оно просто распарсивает функцию и делает из неё генератор.

    Почему они с тем же успехом не могли распарсивать функцию и впиливать поддержку await/async? Если это так хочется делать в рантайме, а не транспилировать.
  • 0

    Парсер классный, аж волосы зашевелились )

    • 0
      Парсер пролучился довольно простой так как он парсит функцию, которая уже откомпилирована движком, а значит имеет корректный синтсксис. Если бы нужно было парсить произвольный текст, то парсер бы раздулся во много раз, и в 35кб он бы не уложился.
      • 0

        Простой? Посмотрите как выглядит нормально и понятно написанный парсер рекурсивного спуска: https://github.com/angrycoding/javascript-parser/blob/master/src/parser/Parser.js

      • 0

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

  • 0

    А так да, велосипед тот ещё, во — первых рантайм, а во — вторых потеря контекста.

  • 0

    Какое уродливое не-решение никаких проблем. Не знаю зачем вы его придумали, и для чего это может пригодиться. Возьмите уж тогда хотя бы promise-hell, всяко менее вырвиглазнее, да и без runtime парсинга js-а.
    Но вот одну интересную мысль из топика я таки выцепил. Вы упомянули о том, что babel-polyfil весит под 100 KiB. Я с возмущением и словами "да не может быть, какая ерунда" полез смотреть и опешил. 97 KiB. Гхм. Грусть-печаль меня охватила.

  • НЛО прилетело и опубликовало эту надпись здесь
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0

        Сравните с этим:


        let action = new Atom( 'action' , ()=> {
            let responses = []
            for( let i = 0 ; i < 4 ; ++i ) responses.push( asyncRequest( i ) )
            responses.forEach( display )
        } )
        action.value()
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0

            Не очень верится, что вы не используете ни одной сторонней библиотеки.

            • НЛО прилетело и опубликовало эту надпись здесь
              • 0

                Что же мешает его добавить?

                • НЛО прилетело и опубликовало эту надпись здесь
                  • 0

                    Это скорее computed паттерн. Есть он много где, но зачастую реализация не эффективна. Оцените масштаб трагедии.

                    • НЛО прилетело и опубликовало эту надпись здесь
                      • 0

                        Про атомы. Тут про них подробнее. Впрочем, CanJS тоже поддерживает "computed".


                        Да, ()=> — это та же лямбда. То же что и function(){ ... }, только сохраняет this.


                        Аккуратно писать на колбэках крайне сложно.

                        • НЛО прилетело и опубликовало эту надпись здесь
                        • НЛО прилетело и опубликовало эту надпись здесь
                          • 0

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

                            • НЛО прилетело и опубликовало эту надпись здесь
                              • +1

                                Я же написал или.

                                • НЛО прилетело и опубликовало эту надпись здесь
                                • НЛО прилетело и опубликовало эту надпись здесь
                                • НЛО прилетело и опубликовало эту надпись здесь
                                  • 0

                                    TortoiseGit, например.

                                    • НЛО прилетело и опубликовало эту надпись здесь
                                        • НЛО прилетело и опубликовало эту надпись здесь
                                        • НЛО прилетело и опубликовало эту надпись здесь
                                          • 0

                                            Для вопросов есть специальный ресурс: https://toster.ru
                                            А так, в Chrome Dev Tools можно смотреть обработчики повешенные на выделенный элемент и всех его предков.

                                            • НЛО прилетело и опубликовало эту надпись здесь
                                              • 0

                                                Там через скоупы можно добраться до нужной функции.

                                                • НЛО прилетело и опубликовало эту надпись здесь
                                                  • 0

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

                                                    • НЛО прилетело и опубликовало эту надпись здесь
                                      • НЛО прилетело и опубликовало эту надпись здесь
                    • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Это решение для очень простой проблемы, так как все итерации однотипные, и не связаны никакой логикой. Такую проблему можно решать легко и через рекурсию, и через Promises, и через async.js. Все становится гораздо сложнее если вам надо делать циклы или условия в каждой итерации, и по их результатам вызывать другие асинхронные функции и ждать колбеков от них
      • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Коллеги, подскажите ненастоящему JS-нику, а способ решения проблемы с callback hell с помощью отказа от инлайнинга функций — годный/в стиле JS?

    Бонусом к этому способу идет соблюдение SRP, нормальное юнит-тестирование и все такое.
    • 0

      К сожалению, часто бывает нужно замыкание.

  • +1
    На данный момент я использую SynJS для написания браузерных тестов

    Т.е. там, где даже мегабайты «лишнего» кода некритичны, да ещё и не продакшен это.


    Ну и даже не заглядывая в исходники ставлю на полную неработоспособность при использовании любого обфускатора.


    P.S. А ведь всё могло быть по другому
    Будущее, судя по всему, за async/await, но пока это будущее не наступило, и многие движки эту возможность не поддерживают.

    Кроме того, Babel тянет за собой около 100 кб минифицированного кода «babel-polyfill», а сконвертированный код работает медленно

    Посмотрев на все это, я решил написать свой велосипед — SynJS свою урезанную версию реализации async/await на голой Эсприме...

    • 0
      Инструмент новый, пока обкатывается в тестах, вроде показывает себя хорошо, а значит будет и в продакшене.

      Насчет обфускатора вы скорее всего правы, но иной раз код с Promises, логикой с рекурсией, циклами и колбеками выглядит так, что никакого обфускатора и не надо.
      • 0
        Ой, ну не надо, продуманная декомпозиция решат всё.

        Забыл добавить: Но, обычно, причина callback/promise hell, банальная лень.

        • 0
          Да, банальная лень, только эта лень разработчиков самого языка JavaScript, которые в течение долгих лет не давали возможность приостанавливать функции нативно и без блокировки, что собственно и породило саму проблему callback hell.
          • +1

            Мы запретили на одном из крупнейших проектов брать анонимные функции вообще (трудно утечки с ними контролировать) и не было никакого колбек-хела, ибо приходилось правильно декомпозировать

      • +1

        Блин, хабр, что ты сделал.


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

        Ой, ну не надо, продуманная декомпозиция решат всё.


        Вот вы пишите про какие-то там задачи, в которых Callback/Promise страшны, но в статье их не приводите, а показываете какие-то элементарные вещи.


        Потом идет пример на async/await, дальше пугаем babel-polyfill (который содержит поддержку генераторов, итераторов и много чего), говорим что спасение есть, это SynJS и… не приводим примера той же задачи на неё, а показываем какую-то муть myTestFunction1 и c setTimeout.


        P.S. Обфускатор (он же и минификатор), сейчас используют все по умолчанию.

  • +4

    Серьезно? Вот это лучше промисов?
    Буду этим джунов пугать!

  • 0
    Почему в каждой статье про «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).

    Я, конечно, могу ошибаться и если это так, буду рад услышать в чем именно я ошибся.
    • +1
      По-моему такой подход решает проблему «callback hell» без каких-либо сторонних библиотек и без нововведений на уровне языка (async/await).

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

    • 0
      Пример из живого кода
          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'));
              }
          }

      Его можно переписать в promise-стиле. Хотя он при этом значительно потеряет в наглядности. А если переписать его на множество разрозненных методов с кучей callback-ов без замыканий, то потребуется куча документации, длинные идиотские названия методов и пр., чтобы в этом вообще не сломать ногу.


      А если этот openChild сам по себе является частью нетривиальной цепочки? Вам придётся плодить много-много сущностей на любой чих, а чтобы многочисленные их методы не путались с друг другом, придётся их ещё изолировать в разные объекты/классы. И так как без контекста такой код будет предельно неочевидным, придётся строчить много-много комментариев, примеров и прочего. А когда выяснится, что половину нужно переделать, т.к. условия изменились… упс. Трагедия.

      • 0
        Спасибо за пример кода, это, наверно, первый адекватный пример, который я увидел, где использование async/await действительно упрощает понимание кода.
        Обычно приводят тривиальные и глупые примеры, которые даже под разряд callback hell не подходят.
      • 0

        Это какой-то, кхм, «странный» метод, где-то просто this.updateHabData, где-то await this.updateHabData, wtf?

        • 0
          Там где с await будет последовательно код идти, там где без — асинхронно бесконтрольно:
          this.updateHabData(this.activeId, { habLoading: true });
          widget = await this.load(id);
          

          — порядок выполнения случайный.

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

            Код с душком, будем честными (проверка на cached вызывает недоумение, как и в целом работа с updateHabData?!).

            • 0

              А в чём собственно проблема? :) Расстановка await перед всеми вызовами updateHabData по сути ничего не поменяет. Это так, экономия на спичках (и муках отладки регенератора в случае чего), наверное избыточная, но никакой трагедии уж точно. Не думал что она кого-то смутит.


              А что не так с проверкой на cached? Она тут нужна ввиду того, что возможны два сценария:


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

              Отдельного упоминания заслуживает onsen-ий navigator.pushPage, которые занимается разного рода анимациями перелистывания экранов (ради чего и приходится городить столько кода).

              • 0

                Для чего два раза id передавать? Для асинхронов логично опционально передавать название события. В целом соглашусь с RubaXa.

                • 0

                  Честно говоря не понял вас :(. Что вы имеете ввиду? И про какие события идёт речь?

              • 0

                Расстановка await в произвольно порядке говорит только о том, что над кодом особо не думали и так сойдёт.


                С cached тоже самое, метод openChild умеет работать с кешем, загружать данные, показывать спиннеры, что-то обновлять (но при этом сам кеш не кладёт), наверно и кофё варить ;] Всё перемешалось в этом методе.

                • 0

                  Повторяю, await расставлены не в произвольном порядке, а в таком, что await-ятся только те операции, дождаться окончания которых критично (тут нет никаких race conditions). Повторюсь, внутри .setState от react-компоненты. Часто вы в своём коде передаёте туда callback? Думаю крайне редко, однако такие ситуации могут быть полезными. В данном случае одна такая в коде показана, т.к. state обязательно должен обновиться до того, как будет вызван onsen.navigator.pushState (иначе он упадёт не найдя нужных для render-а данных).


                  Всё перемешалось в этом методе.

                  Соглашусь только с этим. Но, увы, некоторые UI задачи требуют заморочек. А cache обновляется внутри load-а.

                  • +1

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

                    • 0

                      А вот с этим соглашусь. Серьёзный аргумент. Что с await, что с promise-ми, да даже callback-ми очень просто пустить некоторые ошибки на самотёк. В nodejs для этого даже костыль в виде uncaughtException воткнули.

        • 0

          Я просто расставил await только в тех местах, где критично дождаться окончания его выполнения (там внутри react-ий setState, который может быть обработан асинхронно). Где не критично пустил его на самотёк.

  • 0

    deleted, не туда :(

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