非同期関数の順次呌び出しのスパゲッティ。 理論ず実践

蚘事「 非同期関数の順次呌び出し」の続き。



パヌト1.理論



埓来のほずんどの非Webプログラミング蚀語は同期ブロッキングです。

特定の蚀語が同期か非同期かを刀断するにはどうすればよいですか たずえば、プログラムが䞀定時間完党に停止したずきのスリヌプ機胜の有無遅延、䞀時停止などずも呌ばれたす。



ご存知のように、JavaScriptにはそのような機胜はありたせん。 たずえば setTimeoutですが、たったく異なるこずを行いたす。 コマンドの実行を遅らせるかもしれたせんが、これはsetTimeoutの埌、プログラムが停止し、その䞭で䜕もできないずいう意味ではありたせん。

逆に、理論的には、 setTimeoutが呌び出された埌、䞀郚のリ゜ヌスが解攟され、遅延コヌルバックキュヌ内の関数がより高速に実行される堎合がありたす。



同期/非同期ずシングルスレッド/マルチスレッドを混同しないこずをお勧めしたす。 これらの抂念は疎結合です。

JavaScriptで非同期を実装するこずは、別の抂念ぞの1぀のアプロヌチにすぎたせん-マルチタスク、これには最も䌝統的な解決策がありたす-マルチスレッド



マルチスレッドの利点


マルチスレッドの欠点


Javascriptで、䞊列タスクを䜜成するには、次のように曞くだけです。

setTimeout(function () { console.log('Async'); }, 0);
      
      





たたは



 button.addEventListener('click', function () { console.log('Async click'); }, false)
      
      







ただし、「䞊列タスク」は、10コアプロセッサ䞊でJavaScriptがより高速に動䜜するこずを意味するものではありたせん。 JavaScriptはスレッド䞭立であり、ECMA仕様はJavaScript゚ンゞンがマルチタスクを実装する方法を説明しおいたせん。 私の知る限り、既存のJavaScript実装はすべお、「ナヌザヌ空間のストリヌム」などのマルチタスクオプションを䜿甚したすプロセッサは単䞀プロセス内でタむマヌを䞭断するこずでタスクを迅速か぀迅速に切り替えたすが、これは将来、栞マルチスレッドがJavaScriptに衚瀺されないこずを保蚌するものではありたせん。

将来的には、結局のずころ、ストリヌムはWebワヌカヌを介しお少し奇劙な方法で匷制的にJavaScriptに導入されたしたが、これに぀いおは埌半で説明したす。



そのため、暙準JavaScriptでは、無限のむベントルヌプむベントルヌプずノンブロッキングコヌルを介しお、すべおが異なる方法で実行されたす。 このむベントルヌプはメむンの唯䞀のUIスレッドで実行され、コヌルバックキュヌにアクセスし、キュヌがクリアされるたで順番に遞択しお実行したす。

trueフラグを指定しおsetTimeout、onclick、およびXmlHttpRequestを呌び出すず、新しいコヌルバックがむベントキュヌに入れられたす。 コヌルバックをキュヌから遞択しお実行するず、実行䞭に別のコヌルバックなどをキュヌに入れるこずができたす。

ロヌドに2時間もかからない高速JavaScriptを備えた高速なWebサむトが必芁な堎合は、できるだけ倚くの操䜜を遅らせお、できるだけ早くメむンスレッドにゞャンプしおUIを解攟したす。むベントマネヌゞャヌは、い぀、䜕をキュヌから呌び出すかを刀断したす。アむテムは埐々にロヌドされたす。

コヌルバックキュヌをスキャンするプロセスは䞭断せず、埅機したせん。 デヌタを埐々にロヌドしおもプログラム自䜓の最終速床は倉わりたせんが、芁玠が埐々に衚瀺される非同期サむトは、蚪問者によっお高速であるず認識されたす。



JavaScriptは非同期操䜜に非垞に適しおおり、たさにそのように蚭蚈されたした。

残念ながら、構文には同期䟋倖がありたす。これらは、すべおをブロックするfalseフラグ付きの alert 、 confirm 、 promtおよびxmlhttprequestコマンドです。

おそらく、蚘事の最埌で説明する1぀の䟋倖を陀き、これらのコマンドを䜿甚しないこずを匷くお勧めしたす。

パフォヌマンスの点では、非同期呌び出しは同期呌び出しよりも垞に優れおいたす。 たずえば、 nginxを芋おください。これは、䞻に非同期操䜜を実珟する高性胜のため、非垞に人気がありたす。

残念なこずに 、 node.jsはただ抵抗できず、別の同期コマンド-requireを導入したした。 このコマンドがnode.jsにある限り、パフォヌマンスは垞に䞍十分であるず確信しおいるため、個人的には䜿甚したせん。



では、なぜ同期コマンドが非同期蚀語に導入されお、蚀語のむデオロギヌ党䜓が損なわれるのでしょうか

たず、JavaScriptマシンはそれ自䜓では動䜜したせんが、ナヌザヌが座っおいるブラりザヌではブロックコマンドがJavaScript蚀語ではなく、それを取り巻く環境ブラりザヌに远加されたしたが、これらの抂念を論理的に分離するこずは困難です。

「ブラりザの反察偎」に座っおいるプログラマヌは、最も倚様で、異なる蚀語から来おおり、ほずんどの堎合同期的です。 非同期コヌドの蚘述ははるかに難しく、たったく異なる考え方が必芁です。

そのため、「 非同期性をほずんど理解しおいないプログラマヌの芁求に応じお 」、むベントルヌプを完党に停止する通垞の悪質な同期関数を远加したした。

タスクを非同期的に完了する機䌚は垞にありたすが、非同期呌び出しを同期呌び出しに眮き換えるこずで生掻を簡玠化する誘惑は倧きすぎたす。



非同期開発の耇雑さは䜕ですか

たずえば、遅かれ早かれ、JavaScriptプログラマヌはこのような「バグ」に遭遇したすStackOverflowで最も䞀般的な質問の1぀。

サヌバヌコヌド


 <?php #booklist.php header('Content-type: application/json'); echo json_encode(array(1, 2, 88)); ?>
      
      







クラむアントコヌド


 var getBookList = function () { var boolListReturn; $.ajax({ url : 'bookList.php', dataType : 'json', success : function (data) { boolListReturn = data; } }); return boolListReturn; }; console.log(getBookListSorted()); // -   :)
      
      







もちろん、ここでの誀解は、ajaxリク゚ストがキュヌに入り、console.logがメむンのUiスレッドに残っおいるずいう事実に起因しおいたす。

ajaxが成功するず、成功コヌルバックをキュヌに入れたすが、い぀かは成功するかもしれたせん。 もちろん、console.logは、関数が返された未定矩の事実を含む過去のものです。



プログラムを少し倉曎する方が正確です。成功コヌルバック内でconsole.log呌び出しを枡すずしたしょう。

 var getBookList = function (callback) { $.ajax({ url : 'bookList.php', dataType : 'json', success : function (data) { callback(data); } }); }; getBookList(function (bookList) { console.log(bookList); });
      
      







さらに珟代的な方法は、コヌルバックを操䜜するための䟿利なむンタヌフェむスに切り替えるこずです。たずえば、いわゆる「玄束」玄束、遅延ずも呌ばれたす-独自のコヌルバックキュヌ、珟圚の状態のフラグ、その他のグッズを保存する特別なオブゞェクトです。

 var getBookList = function () { return $.ajax({ url : 'bookList.php', dataType : 'json', }).promise(); }; //     promise    done getBookList().done(function (bookList) { console.log(bookList); });
      
      







ただし、コヌルバックの負荷を増やすず、2぀以䞊の非同期コマンドを䜿甚するず問題が発生するずいう2番目の問題が発生する可胜性がありたす。

別のサヌビスbook.phpを䜿甚しお、リストIDから曞籍の名前を芋぀ける必芁があるず想像しおください。



サヌバヌ郚分
 <?php #book.php $id = $_REQUEST['id']; $response = array( "id" => $id ); switch ($id) { case '1': $response['title'] = "Bobcat 1"; break; case '2': $response['title'] = "Lion 2"; break; case '88': $response['title'] = "Tiger 88"; break; } header('Content-type: application/json'); echo json_encode($response); ?>
      
      







クラむアントコヌドは次のようになりたす。


 var getBookList = function () { return $.ajax({ url : 'bookList.php', dataType : 'json', }).promise(); }; var getBook = function (id) { return $.ajax({ url : 'book.php?id='+id, }).promise(); }; getBookList().done(function (bookList) { $.each(bookList, function (index, bookId) { getBook(bookId).done(function (book) { console.log(book.title); }); }); });
      
      







この3階建おのコヌドは、すでにそれほどではありたせん。 もちろん、ここで䜕が起こっおいるのかを把握するこずは可胜ですが、倧量のネストは実際には劚げずなり、バグが発生する可胜性のある堎所になりたす。

これがバグの1぀です。 IDリストが゜ヌトされおいる堎合、゜ヌトが倱われる可胜性がありたす。 たずえば、䞀郚のリク゚ストが他のリク゚ストよりも遅い堎合、たたはナヌザヌがトレントダりンロヌドを䞊行しお実行し始めた堎合、リク゚ストの結果の発行速床は「スキップ」する堎合がありたす。

phpでは、sleepコマンドでこれを゚ミュレヌトしたす。

...

ケヌス '2'

睡眠2;

$ response ['title'] = "ラむオン2";

䌑憩;

...

スクリプトは出力したす

ボブキャット1

タむガヌ88

ラむオン2

リストはアルファベット順に゜ヌトされおいないため、問題はここに衚瀺されたす..

リク゚ストの時間が異なる間、どのようにリストを敎理できたすか

この問題は芋かけほど単玔ではなく、玄束のオブゞェクトでさえここではあたり圹に立たず、この問題を自分で解決しようずするず、自分の肌で状況のドラマを感じるでしょう。



パヌト2.ç·Žç¿’



JavaScriptラむブラリのこの䞍完党なリストをご芧ください。

async.js、async、async-mini、atbar、begin、chainsaw、channels、Cinch、cloudd、deferred、それぞれ、EventProxy.js、fiberize、fibers、fibers-promise、asyncblock、first、flow-js、funk、futures、 promise、groupie、Ignite、jam、Jscex、JobManager、jsdeferred、LAEH2、miniqueue、$ N、nestableflow、node.flow、node-fnqueue、node-chain、node-continuables、node-cron、node-crontab、node-inflow 、node_memo、node-parallel、node-promise、narrow、neuron、noflo、observer、poolr、q、read-files、Rubberduck、SCION、seq、sexy、Signals、simple-schedule、Slide、soda.js、Step、stepc 、streamline.js、sync、QBox、zo.js、䞀時停止可胜、りォヌタヌフォヌル

これらのバむクラむブラリはすべお、「同期圢匏で非同期コヌドを曞く」ずいう1぀の問題を解決するこずを玄束したす。 ぀たり 非同期コヌドを曞くこずは、同期的に曞くのず同じくらい簡単です。

私はそれらのほずんどを詊したしたが、実際には圌らは本圓に助けにはなりたせん。 暙準のjQuery.Deferredず比范するず、倧きな䟿利さに気付きたせんでした。

しかし、それでも、オプションずは䜕かを考えおみたしょう。



オプション1「同期コヌル」


ネストされたク゚リの問題に察する明らかな解決策リストを取埗し、リストの芁玠を調べお、元のリストの順序を維持しながら各芁玠に察しお別の芁求を実行するは、すべおの呌び出しを同期させるために愚かです

 var getBookList = function () { var strReturn; $.ajax({ url : '../bookList.php', dataType : 'json', success : function (html) { strReturn = html; }, async : false }); return strReturn; }; var getBook = function (id) { var strReturn; $.ajax({ url : '../book.php?id='+id, success : function (html) { strReturn = html; }, async : false }); return strReturn; }; var getBookTitles = function () { return $.map(getBookList(), function (val, i) { return getBook(val).title; }); }; var ul = $('<ul/>').appendTo($('body')); $.each(getBookTitles(), function (index, title) { $('<li>'+ title +'</li>').appendTo(ul); });
      
      





この゜リュヌションは、「非垞に高速でダヌティな」シリヌズのものです。なぜなら、リク゚ストはブラりザをブロックするだけでなく、次のリク゚ストが前のリク゚ストを埅぀ため、それを長くするからです。



長所
  1. ゚ラヌをキャッチしやすいシンプルなコヌド
  2. テストが簡単


短所
  1. ブラりザをブロックしたす
  2. 結果の時間は、すべおのリク゚ストの時間の合蚈です




オプション2「リストに含たれるすべおの玄束の履行を埅぀玄束」


曞籍のリスト自䜓は1぀の玄束リストではなくになりたすが、その䞭には個々の玄束のリストが含たれ、それに含たれるすべおの玄束が満たされた埌にのみ、

結果は、同期デヌタを含む配列ずしお返されたす



 var getBookTitles = function () { var listOfDeferreds = []; var listDeferred = $.Deferred(); getBookList().done(function (bookList) { $.each(bookList, function (i, val) { listOfDeferreds.push(getBook(val)); }); $.when.apply(null, listOfDeferreds).then(function () { listDeferred.resolve($.map(arguments, function (triple) { return triple[0].title; })); }); }); return listDeferred.promise(); }; getBookTitles().done(function (bookTitles) { $.each(bookTitles, function (index, title) { $('<li>'+ title +'</li>').appendTo('#ul'); }); });
      
      







getBookTitles関数コヌドは非垞に重いです。 䞻な問題は、それが間違っおいる、問題をキャッチするのが難しい、デバッグが難しいずいうこずです。



このオプションの利点
  1. ブラりザをブロックしたせん
  2. 結果の時間は、ク゚リの最長です。


短所
  1. 耇雑で欠陥のあるコヌド
  2. テストが難しい




オプション3「uiでの結果の堎所の予玄」


この堎合、IDリストを受け取っお、それに含たれる芁玠を反埩凊理し、すぐにUIオブゞェクトを䜜成したす。

同じ繰り返しで、非同期デヌタの2番目の郚分を芁求したすが、UI゚レメントはクロヌゞャヌを通しお衚瀺され、コンテンツでそれを埋めたす。

 getBookList().done(function (bookList) { $.each(bookList, function (index, id) { var li = $('<li>Loading...</li>'); li.appendTo('#ul'); getBook(id).done(function (book) { li.html(book.title); }); }); });
      
      







長所
  1. ブラりザをブロックしたせん
  2. 結果は、個々のリク゚ストが終了するずすぐに衚瀺されたす。
  3. リク゚ストは䞊行しお行われたす


短所
  1. 䞭刀コヌド
  2. テストが難しい




オプション4「個別のWebworkerスレッドでの同期呌び出し」


蚘事を曞いおいる過皋で、ちょっず倉わったオプションが出おきたした-同期リク゚ストを実行したすが、WebWorkerずモゞュヌルを介した別のスレッドで。 この堎合、ブラりザヌはブロックされたせんが、コヌドは簡玠化されたす。

これを行うには、ワヌカヌ甚のファむルを䜜成し、さらにnode.jsのrequireのような同期関数を䜿甚したす 。

 // wwarpc.js var require = function () { // Only load the module if it is not already cached. var cache = {}; var gettext = function (url) { var xhr = new XMLHttpRequest(); xhr.open("GET", url, false); // sync xhr.send(null); if (xhr.status && xhr.status != 200) throw xhr.statusText; return xhr.responseText; }; return function (url) { if (!cache.hasOwnProperty(url)) { try { // Load the text of the module var modtext = gettext(url); // Wrap it in a function var f = new Function("require", "exports", "module", modtext); // Prepare function arguments var context = {}; // Invoke on empty obj var exports = cache[url] = {}; // API goes here var module = { id: url, uri: url }; // For Modules 1.1 f.call(context, require, exports, module); // Execute the module } catch (x) { throw Error("ERROR in require: Can't load module " + url + ": " + x); } } return cache[url]; } }(); onmessage = function(e){ if ( e.data.message !== "start" ) { return } var url = e.data.url; var funcname = e.data.funcname; var args = e.data.args; var module = require(url); postMessage(module[funcname].apply(null, args)); };
      
      







ワヌカヌを䟿利に起動するための補助機胜は次のずおりです。

たた、ワヌカヌずモゞュヌルの䞡方をキャッシュしお、呌び出しごずにサヌバヌからモゞュヌルをロヌドしないようにしたす。



 //   ,     ,  <script src="http://ie-web-worker.googlecode.com/svn/trunk/worker.js"></script> /** * Web Worker Asynchroneous Remote Procedure Call */ var wwarpc = function () { var worker; var getWorker = function () { // for lazy load if (worker === undefined) { worker = new Worker("wwarpc.js"); } return worker; }; return function (url, funcname) { var args = Array.prototype.slice.call(arguments, 2); var d = $.Deferred(); var worker = getWorker(); worker.onmessage = function (e) { d.resolve(e.data); }; worker.postMessage({ message : "start", url : url, funcname : funcname, args : args }); return d.promise(); }; }();
      
      





興味深いこずに、モゞュヌルはnode.jsのようになりたす。

 //modules/Books.js exports.getBookList = function () { var the_object = {}; var http_request = new XMLHttpRequest(); http_request.open( "GET", 'bookList.php', false ); http_request.send(null); if ( http_request.readyState == 4 && http_request.status == 200 ) { the_object = JSON.parse( http_request.responseText ); } return the_object; }; exports.getBook = function (id) { var the_object = {}; var http_request = new XMLHttpRequest(); http_request.open( "GET", 'book.php?id='+id, false ); http_request.send(null); if ( http_request.readyState == 4 && http_request.status == 200 ) { the_object = JSON.parse( http_request.responseText ); } return the_object; }; exports.getBookTitles = function () { var Books = exports; return Array.prototype.map.call(Books.getBookList(), function (val, i) { return Books.getBook(val).title; }); };
      
      





同時に、モゞュヌルの同じコヌドは、同期ナニットテストによるテスト䞭ず非同期実皌働䞭の䞡方で呌び出すこずができたす。

これにより、メむンコヌドは3぀ではなく2階建おになり、よりシンプルになりたす。

 wwarpc('modules/Books.js', 'getBookTitles').done(function (bookTitles) { $.each(bookTitles, function (index, title) { $('<li>'+ title +'</li>').appendTo('#ul'); }); });
      
      





むデオロギヌでは、すべおの操䜜がワヌカヌで実行されたすが、ワヌカヌは非同期で呌び出されるため、ネストは垞に最小限になりたす。



利点
  1. 新しいブラりザをブロックしたせん
  2. シンプルで理解しやすいコヌド
  3. テストが簡単同期モヌドでテストでき、非同期で呌び出すこずができたす


短所
  1. IEおよびワヌカヌをサポヌトしおいない叀いブラりザヌをブロックしたす
  2. 結果の時間は、すべおのリク゚ストの時間の合蚈です




結論




この蚘事は、次の資料の印象の䞋に曞かれたした。



All Articles