📜 ⬆️ ⬇️

Another bike to fight callback hell in javascript



It is believed that the JavaScript world is booming: new language standards are regularly published, new syntactic features appear, and developers immediately adapt and rewrite all their frameworks, libraries, and other projects so that all this is used. Now, for example, if you still write in the code var, and not const or let, then this is sort of like a moveton. And if the function is not described using the arrow syntax, then it's a shame ...

However, all these const-s, let-s, class-s and most other innovations are nothing more than cosmetics, which, although it makes the code more beautiful, does not solve really acute problems.

I think that the main problem of JavaScript, which has long been ripe and overripe, and which should have been resolved in the first place, is the impossibility of suspending execution, and as a result, the need to do everything through callbacks.
')

What are good callbacks?


In my opinion, only by giving us eventfulness and asynchrony, which allows us to instantly respond to events, do a great job in one process, save resources, etc.

What are bad callbacks?


The first thing a newbie usually encounters is the fact that as the complexity grows, the code quickly turns into incomprehensible, repeatedly embedded blocks - “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){ ... }); }); } }); 

Secondly, if functions with callbacks are connected to each other by logic, then this logic has to be split up and put into separate named functions or modules. For example, the code above will execute the “for” loop and start the fetch set (array_of_urls [i] ... instantly, and if array_of_urls is too large, then the JavaScript engine will hang and / or crash with an error.

This can be dealt with by rewriting the “for” loop into a recursive function with a callback, but recursion can overflow the stack and also drop the engine. In addition, recursive programs are more difficult to understand.

Other solutions require the use of additional tools or libraries:


The future, apparently, for async / await, but so far this future has not come, and many engines do not support this feature.

In order to be able to execute code with async / await on JavaScript 2015 engines that are currently relevant, transpilers were created - code converters from new JavaScript to old. The most famous of them, Babel, allows you to convert Javascript 2017 code with async / await in JavaScript 2015 and run it on almost all engines currently used.

It looks like this:

JavaScript 2017 source code:

 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"); } } 

Converted JavaScript code 2015:

Hidden in spoiler
 "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"); }); }; } 


To be able to debug such code, you need to configure and use much of what is listed in this article .

All this in itself requires nontrivial efforts. In addition, Babel draws about 100 kb of the minified code "babel-polyfill", and the converted code works slowly (which is indirectly hinted at by the numerous constructions case number_string in the generated code).

Looking at all this, I decided to write my bike - SynJS. It allows you to write and synchronously execute code with callbacks:

 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"; } 

You can execute the function as follows:

 SynJS.run(myTestFunction1,null, function (ret) { console.log('done all:', ret); }); 

The result will be:

 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) 

Compared to Babel, he:


SynJS takes a pointer to a function as a parameter, parses this function into individual operators (parses the nested operators recursively, if necessary), wraps them all in a function, and places these functions in a tree structure equivalent to the function code. Then, an execution context is created in which local variables, parameters, current stack status, program counters, and other information needed to stop and continue execution are stored. After that, the statements in the tree structure are executed one by one, using the context as the data store.

The function can be performed via SynJS as follows:

 SynJS.run(funcPtr,obj, param1, param2 [, more params],callback) 

Options:

- funcPtr: a pointer to the function to be executed synchronously
- obj: an object that will be available in the function through this
- param1, param2: parameters
- callback: function to be executed upon completion

In order to wait for the completion of the callback in SynJS, there is a SynJS.wait () operator that allows you to stop the execution of the function launched via SynJS.run (). The operator can take 3 forms:

- SynJS.wait () - stops execution until SynJS.resume () is called
- SynJS.wait (number_of_milliseconds) - suspends execution for the time number_of_milliseconds
- SynJS.wait (some_non_numeric_expr) - checks (!! some_non_numeric_expr), and stops the execution if false.

With SynJS.wait, you can expect the completion of one or more callbacks:

  var cb1, cb2; setTimeout(function () { cb1 = true; SynJS.resume(_synjsContext); }, 1000); setTimeout(function () { cb2 = true; SynJS.resume(_synjsContext); }, 2000); SynJS.wait(cb1 && cb2); 

To give a signal about the completion of the callback in the main thread, use the function

SynJS.resume (context)

The required context parameter contains a link to the execution context that needs to be notified (since each call to SynJS.run creates and runs a separate context, there can be several running contexts in the system at the same time).

When parsing, SynJS wraps each statement into a function as follows:

 function(_synjsContext) { ...   ... } 

Thus, the _synjsContext parameter in the callback code can be used to signal the completion:

 SynJS.resume(_synjsContext); 

Handling local variables.


When parsing the body of a function, SynJS determines the declarations of local variables by the keyword var, and creates a hash for them in the execution context. When wrapping a function, the operator code is modified, and all references to local variables are replaced with references to the hash in the execution context.

For example, if the source statement in the function body looked like this:
  var i, res; ... setTimeout(function() { res = 'i='+i; SynJS.resume(_synjsContext); },1000); 

then the operator wrapped in the function will look like this:

 function(_synjsContext) { setTimeout(function() { _synjsContext.localVars.res = 'i='+_synjsContext.localVars.i; SynJS.resume(_synjsContext); },1000); } 

Some examples of using SynJS

1. Select from the database an array of parent records, for each of them get a list of children .

2. According to the list of URLs, receive them one by one until the content of the URL satisfies the condition.

code
  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. In the database, bypass all children, grandchildren, etc. some parent.

Code
  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); }); 


At the moment I use SynJS to write browser tests in which I want to imitate complex user scripts (click "New", fill out a form, click "Save", wait, check what was written through the API, etc.) - SynJS allows you to reduce code, and most importantly, increase its clarity.

I hope that someone will also find it useful until the bright future has come with async / await.

→ Project on githaba
→ NPM

PS I almost forgot, SynJS has an operator SynJS.goto (). Why not?

Source: https://habr.com/ru/post/319094/


All Articles