JavaScriptでコールバック地獄に対処する別の自転車





JavaScriptの世界は活況を呈していると考えられています:新しい言語標準が定期的にリリースされ、新しい構文チップが登場し、開発者はこれをすべて即座に適応させ、フレームワーク、ライブラリ、およびその他のプロジェクトをすべて使用できるように書き換えます。 さて、例えば、あなたがまだconstの代わりにvarを書いているか、コードにletを書いているなら、これはすでに悪いマナーのようなものです。 また、関数が矢印構文で記述されていない場合、一般的には残念です...



ただし、これらのconst、let、s、および他のほとんどの革新はすべて化粧品に過ぎず、コードをより美しくしますが、本当に深刻な問題を解決しません。



JavaScriptの主な問題は、長い間熟していて熟しており、そもそも解決すべきだったのは、実行を一時停止できないことであり、その結果、コールバックを通じてすべてを行う必要があると思います。



良いコールバックとは何ですか?



私の意見では、それらが私たちに偶然性と非同期性を与えるという事実によってのみ、私たちはイベントに即座に対応し、1つのプロセスで多くの仕事をし、リソースを節約するなどを可能にします。



悪いコールバックとは何ですか?



初心者が通常最初に遭遇することは、複雑さが増すと、コードがすぐにあいまいな繰り返しネストされたブロックに変わるという事実です-「コールバック地獄」:



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」ループを実行し、大量のフェッチを実行します(array_of_urls [i] ...瞬時に、またarray_of_urlsが大きすぎる場合、JavaScriptエンジンがフリーズおよび/またはクラッシュします。



これに対処するには、「for」ループをコールバック付きの再帰関数に書き直しますが、再帰によってスタックがオーバーフローし、エンジンがドロップする可能性があります。 さらに、再帰プログラムは理解しにくいです。



他のソリューションでは、追加のツールまたはライブラリを使用する必要があります。





将来は、明らかに非同期/待機のためですが、これまでのところこの未来は到来しておらず、多くのエンジンはこの機能をサポートしていません。



現在関連するJavaScript 2015エンジンで非同期/待機を使用してコードを実行できるようにするために、トランスパイラーが作成されました。新しいJavaScriptから古いJavaScriptへのコードコンバーターです。 最も有名なBabelでは、Javascript 2017コードを非同期/待機から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 kbの縮小されたbabel-polyfillコードをプルし、変換されたコードの実行は遅くなります(生成されたコードの多数の-line_numberコンストラクトによって間接的に示唆されます)。



これらすべてを見た後、私は自分の自転車-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)
      
      





バベルと比較して、彼は:





SynJSは、関数へのポインターをパラメーターとして受け取り、この関数を個別のステートメントに解析し(必要に応じてネストされたステートメントを再帰的に解析します)、それらをすべて関数でラップし、これらの関数を関数コードに相当するツリー構造に配置します。 次に、ローカル変数、パラメーター、スタックの現在の状態、プログラムカウンター、および実行の停止と継続に必要なその他の情報が格納される実行コンテキストが作成されます。 その後、コンテキストをデータストアとして使用して、ツリー構造内のステートメントが次々に実行されます。



関数は、次のようにSynJSを介して実行できます。



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





パラメータ:



-funcPtr:同期的に実行される関数へのポインター

-obj:これを通して関数で利用できるオブジェクト

-param1、param2:オプション

-コールバック:完了時に実行される関数



コールバックが完了するまで待機できるように、SynJS.wait()がSynJSに存在します。これにより、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を使用すると、1つ以上のコールバックが完了することが期待できます。



  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(コンテキスト)



必要なコンテキストパラメータには、通知する必要がある実行コンテキストへの参照が含まれます(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のコンテンツが条件を満たすまで1つずつ受信します。



コード
  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を使用して、複雑なユーザースクリプトをシミュレートするブラウザーテストを作成します(「新規」をクリックし、フォームに入力し、「保存」をクリックし、待機し、APIを介して記述内容を確認するなど)-SynJSにより、コード、そして最も重要なことは、その理解度を高めます。



非同期/待ち合わせで明るい未来が来るまで、誰かがそれも役に立つことを願っています。



githubでのプロジェクト

NPM



PS私はほとんど忘れていました、SynJSには演算子SynJS.goto()があります。 どうして?



All Articles