非同期?
非同期(Asynchronous)という言葉を色々なところで目にしますが、この概念をよく理解していないと「もうわかっている」と思われてしまうことが多いです(* ̄? ̄) )。
同じような状況の方はこの単語で検索していただければ大まかな説明がわかります。ここでJavaScriptの非同期性について少し補足説明します。
このコードを見てください:
var start = new Date(); setTimeout(function(){ var end = new Date(); console.log("Time elapsed: ", end - start, "ms"); }, 500); while (new Date - start < 1000) {};
このコードを実行すると、経過時間: 1013ms のような結果が得られます。 setTimeout() によって 500 ミリ秒後に実行されるように設定された関数は、実際には実行前に 1000 ミリ秒以上待機します。
どう説明すればいいでしょうか? setTimeout() が呼び出されると、遅延イベントがキューに入れられます。次に、コードがなくなるまで、この後のコードとその次のコードの実行を続けます。コードがなくなると、JavaScript スレッドはアイドル状態になります。このとき、JavaScript 実行エンジンはキューを調べて、キュー内で「トリガーされるべき」イベントを見つけて、このイベントのハンドラー (関数) を呼び出します。プロセッサは実行を完了するとキューに戻り、次のイベントを調べます。
シングルスレッドの JavaScript は、キューを介したイベント ループの形式で動作します。したがって、前のコードでは、コードの実行中に while を使用して実行エンジンを最大 1000 ミリ秒ドラッグし、すべてのコードが完了してキューに返されるまでイベントはトリガーされません。これがJavaScriptの非同期の仕組みです。
JavaScript の非同期の問題
JavaScript の非同期操作は、必ずしも簡単であるとは限りません。
Ajax は、おそらく私たちが最もよく使用する非同期操作です。 jQuery を例にとると、Ajax リクエストを開始するコードは通常次のようになります。
// Ajax请求示意代码 $.ajax({ url: url, data: dataObject, success: function(){}, error: function(){} });
この書き方に何か問題はありますか?簡単に言えば、携帯性が不十分です。リクエストが開始される場所で成功コールバックとエラーコールバックを記述する必要があるのはなぜですか?コールバックで非常に多くのことを実行する必要がある場合、1 つのことを思いついたときに、ここで実行してコードを追加する必要がありますか?
次のようなことを完了したいとします。Ajax アクセスには 4 つの URL アドレスがあり、最初のアクセスが完了した後、最初のアドレスにアクセスする必要があります。として取得されたデータ 2 番目のパラメータに再度アクセスし、2 回目のアクセスが完了した後、3 番目のパラメータにアクセスします。... この時点で、4 つのアクセスがすべて完了します。この書き方によれば、次のようになります。
$.ajax({ url: url1, success: function(data){ $.ajax({ url: url2, data: data, success: function(data){ $.ajax({ //... }); } }); } })
この Pyramid of Doom というコードは次のように見えると間違いなく思われるでしょう。ひどい。直接アタッチされたコールバックの作成に慣れている場合は、次から次へと渡される非同期イベントについて混乱を感じるかもしれません。これらのコールバック関数に個別に名前を付け、個別に保存すると、フォーム内のネストが減り、コードが明確になりますが、それでも問題は解決されません。
もう 1 つの一般的な問題は、2 つの Ajax リクエストを同時に送信し、両方のリクエストが正常に返された後に次のことを行うことです。前の方法に従って次のことを行う場合について考えてみましょう。コールバックを呼び出し位置にアタッチするのは少し難しいように思えますか?
Promise は、これらの非同期操作の処理に適しており、より洗練されたコードを作成できます。
プロミスがステージに登場
プロミスとは何ですか?引き続き、前の jQuery Ajax リクエスト信号コードを例として取り上げます。このコードは、実際には次のように記述できます。
var promise = $.ajax({ url: url, data: dataObject }); promise.done(function(){}); promise.fail(function(){});
これは、前のものと似ています。 Ajax リクエスト信号のコードは同等です。ご覧のとおり、Promise を追加するとコードの形式が変わります。 Ajax リクエストは、変数の割り当てと同様に「保存」されます。これがカプセル化であり、カプセル化によって非同期イベントが本当に簡単になります。
カプセル化は便利です
Promise オブジェクトは、非同期イベントへのカプセル化された参照のようなものです。この非同期イベントが完了した後に何かをしたいですか?コールバックをアタッチするだけで、いくつアタッチしても問題ありません。
jQuery の Ajax メソッドは Promise オブジェクトを返します (これは jQuery 1.5 で追加された重要な機能です)。非同期イベントが正常に完了した後に実行したい 2 つの関数 do1() と do2() がある場合、これを行うだけで済みます:
promise.done(do1); // Other code here. promise.done(do2);
この方法では、非同期イベントが開始される場所に関係なく、この Promise オブジェクトを保存し、コードの作成中にいつでも任意の数のコールバックをアタッチするだけで済みます。これがプロミスのメリットです。
正式な紹介
Promise は非同期操作に非常に便利であるため、Promises/A と呼ばれる CommonJS の仕様として開発されました。 Promise は、操作が完了した後の戻り値を表します。
正 (実現または解決) で、Promise 操作が成功したことを示します。
否定 (拒否または失敗) は、Promise 操作が失敗したことを示します。
待機中 (保留中)、まだ陽性または陰性の結果が得られておらず、進行中です。
さらに、Promise 操作が成功したか失敗したかを示すために使用される名目上の状態があり、これは Settled と呼ばれる、正の状態と負の状態の集合です。 Promise には次の重要な機能もあります:
一个Promise只能从等待状态转变为肯定或否定状态一次,一旦转变为肯定或否定状态,就再也不会改变状态。
如果在一个Promise结束(成功或失败,同前面的说明)后,添加针对成功或失败的回调,则回调函数会立即执行。
想想Ajax操作,发起一个请求后,等待着,然后成功收到返回或出现错误(失败)。这是否和Promise相当一致?
进一步解释Promise的特性还有一个很好的例子:jQuery的$(document).ready(onReady)。其中onReady回调函数会在DOM就绪后执行,但有趣的是,如果在执行到这句代码之前,DOM就已经就绪了,那么onReady会立即执行,没有任何延迟(也就是说,是同步的)。
Promise示例
生成Promise
Promises/A里列出了一系列实现了Promise的JavaScript库,jQuery也在其中。下面是用jQuery生成Promise的代码:
var deferred = $.Deferred(); deferred.done(function(message){console.log("Done: " + message)}); deferred.resolve("morin"); // Done: morin
jQuery自己特意定义了名为Deferred的类,它实际上就是Promise。$.Deferred()方法会返回一个新生成的Promise实例。一方面,使用deferred.done()、deferred.fail()等为它附加回调,另一方面,调用deferred.resolve()或deferred.reject()来肯定或否定这个Promise,且可以向回调传递任意数据。
合并Promise
还记得我前文说的同时发送2个Ajax请求的难题吗?继续以jQuery为例,Promise将可以这样解决它:
var promise1 = $.ajax(url1), promise2 = $.ajax(url2), promiseCombined = $.when(promise1, promise2); promiseCombined.done(onDone);
$.when()方法可以合并多个Promise得到一个新的Promise,相当于在原多个Promise之间建立了AND(逻辑与)的关系,如果所有组成Promise都已成功,则令合并后的Promise也成功,如果有任意一个组成Promise失败,则立即令合并后的Promise失败。
级联Promise
再继续我前文的依次执行一系列异步任务的问题。它将用到Promise最为重要的.then()方法(在Promises/A规范中,也是用“有then()方法的对象”来定义Promise的)。代码如下:
var promise = $.ajax(url1); promise = promise.then(function(data){ return $.ajax(url2, data); }); promise = promise.then(function(data){ return $.ajax(url3, data); }); // ...
Promise的.then()方法的完整形式是.then(onDone, onFail, onProgress),这样看上去,它像是一个一次性就可以把各种回调都附加上去的简便方法(.done()、.fail()可以不用了)。没错,你的确可以这样使用,这是等效的。
但.then()方法还有它更为有用的功能。如同then这个单词本身的意义那样,它用来清晰地指明异步事件的前后关系:“先这个,然后(then)再那个”。这称为Promise的级联。
要级联Promise,需要注意的是,在传递给then()的回调函数中,一定要返回你想要的代表下一步任务的Promise(如上面代码的$.ajax(url2, data))。这样,前面被赋值的那个变量才会变成新的Promise。而如果then()的回调函数返回的不是Promise,则then()方法会返回最初的那个Promise。
应该会觉得有些难理解?从代码执行的角度上说,上面这段带有多个then()的代码其实还是被JavaScript引擎运行一遍就结束。但它就像是写好的舞台剧的剧本一样,读过一遍后,JavaScript引擎就会在未来的时刻,依次安排演员按照剧本来演出,而演出都是异步的。then()方法就是让你能写出异步剧本的笔。
将Promise用在基于回调函数的API
前文反复用到的$.ajax()方法会返回一个Promise对象,这其实只是jQuery特意提供的福利。实际情况是,大多数JavaScript API,包括Node.js中的原生函数,都基于回调函数,而不是基于Promise。这种情况下使用Promise会需要自行做一些加工。
这个加工其实比较简单和直接,下面是例子:
var deferred = $.Deferred(); setTimeout(deferred.resolve, 1000); deferred.done(onDone);
这样,将Promise的肯定或否定的触发器,作为API的回调传入,就变成了Promise的处理模式了。
Promise是怎么实现出来的?
本文写Promise写到这里,你发现了全都是基于已有的实现了Promise的库。那么,如果要自行构筑一个Promise的话呢?
位列于Promises/A的库列表第一位的Q可以算是最符合Promises/A规范且相当直观的实现。如果你想了解如何做出一个Promise,可以参考Q提供的设计模式解析。
限于篇幅,本文只介绍Promise的应用。我会在以后单独开一篇文章来详述Promise的实现细节。
作为JavaScript后续版本的ECMAScript 6将原生提供Promise,如果你想知道它的用法,推荐阅读JavaScript Promises: There and back again。
结语
約束という言葉はあまりにも頑固なので翻訳には適しておらず、一見しただけでは意味がわかりません。ただし、JavaScript でより複雑な非同期タスクを実行する場合には、実際にかなりの助けになります。
上記は、非同期 JavaScript プログラミング_node.js?1.1.5 での Promise の使用方法の内容です。さらに関連する内容については、PHP 中国語 Web サイト (m.sbmmt.com) に注目してください。