この記事では、非同期の結果、コールバック関数、遅延オブジェクト、およびそれらの機能を返すための基本的なアプローチについて説明します。 JavaScriptでサンプルが提供され、遅延サンプルオブジェクトが解析されます。 この記事は、非同期プログラミングを理解し始めるプログラマー、および非同期プログラミングに精通しているが、遅延オブジェクトを所有していないプログラマーに役立ちます。
同期および非同期呼び出し
すべての関数は、同期および非同期形式で記述できます。 何らかの計算を行う
calc
関数があるとします。
通常の「同期」アプローチの場合、計算結果は戻り値を介して送信されます。つまり、結果は関数が実行された直後に利用可能になり、別の計算で使用できます。
var result = calc(); another_calc(result * 2);
コードは厳密に連続して実行され、1行で得られた結果は次の行で使用できます。 これは、次のステートメントが前のステートメントから論理的に続く場合の定理の証明を連想させます。
非同期呼び出しの場合、結果を適切に取得できません。
calc
関数を呼び出して、計算を実行してその結果を取得する必要があることのみを示します。 この場合、次の行は前の行が完了するのを待たずに実行を開始します。 それでも、どうにかして結果を取得する必要があり、ここでコールバックが役立ちます。これは、計算結果の到着時にシステムによって呼び出される関数です。 結果は引数としてこの関数に渡されます。
calc(function (result) { another_calc(result * 2); }); no_cares_about_result();
例からわかるように、関数にはシグネチャ
calc(callback)
があり、
callback
は結果を最初のパラメーターとして受け取ります。
calc
は非同期に実行されるため、
no_cares_about_result
関数
no_cares_about_result
結果にアクセスできず、一般的に言えば、コールバックよりも早く実行できます(特にJavaScriptについて言えば-呼び出された関数が本当に非同期であるが、キャッシュからデータを取得しない場合など)呼び出し元のコードが実行された後に常に実行されることが保証されています。つまり、コールバックの前に残りのコードが常に実行されます。これについては以下で説明します)。
そのようなコードは、その「直接的な」同期アナログと同じセマンティックの負荷で、すでに多少理解が難しくなっていることを認めなければなりません。 非同期アプローチを使用する利点は何ですか? まず、システムリソースの合理的な使用において。 たとえば、
calc
が時間がかかる計算である場合、または外部リソースを使用する場合、その使用には一定の遅延が生じるため、同期アプローチでは、後続のすべてのコードは結果を待機するように強制され、
calc
実行されるまで実行されません。 非同期アプローチを使用すると、コードのどの部分が結果に依存し、どの部分が結果に影響されないかを明示的に示すことができます。 この例では、
no_cares_about_result
は結果を明示的に使用しないため、結果を期待する必要はありません。 コールバック内のコードセクションは、結果を受け取った後にのみ実行されます。
一般的に、ほとんどのAPIは本質的に非同期ですが、同期リソースを模倣できます。リモートリソースへのアクセス、データベースクエリ、ファイルAPIでも非同期です。 APIが同期を「装う」場合、そのような「見せかけ」の成功は、結果の遅延に関連付けられます。遅延が小さいほど良いです。 ローカルマシンで動作する同じファイルAPIはわずかな遅延を示し、多くの場合同期として実装されます。 リモートリソースの操作とデータベースへのアクセスは、ますます非同期的に実装されます。
階層化された課題
非同期呼び出しの難しさは、非同期呼び出しを行うだけでなく、その結果を受け取って、それを使用して別の非同期呼び出しで使用する必要がある場合に、より顕著になります。 明らかに、連続して実行されるいくつかのコード行への同期アプローチは、ここでは適切ではありません。
var result = calc_one(); result = calc_two(result * 2); result = calc_three(result + 42); // using result
コードは次の形式を取ります。
calc_one(function (result) { calc_two(result * 2, function (result) { calc_three(result + 42, function (result) { // using result }); }); });
第一に、このコードは「マルチレベル」になりましたが、アクションの点では同期に似ています。 第二に、
calc_two
、
calc_three
関数のシグニチャーでは、 入力パラメーターとコールバックが混在しています。実際には、 結果が返される場所、つまり出力パラメーターです。 第三に、各関数が失敗し、結果が得られない場合があります。
このコードは、コールバック関数を個別に定義し、名前で渡すことで簡素化できますが、これはすべての問題の解決策ではありません。 ここでは新しいレベルの抽象化が必要です。つまり、 非同期の結果を抽象化できます。
非同期結果
そのような結果は何ですか? 実際、これは結果がいつか来るか、すでに到着しているという情報を含むオブジェクトです。 結果へのサブスクライブは同じコールバックを介して実行されますが、このオブジェクトにカプセル化され、非同期関数が入力パラメーターとしてコールバックを実装することを義務付けなくなりました。
実際、結果オブジェクトには次の3つのことが必要です:結果をサブスクライブする機能、結果の到着を示す機能(これはAPIクライアントではなく非同期関数自体によって使用されます)を実装し、この結果を保存する
そのようなオブジェクトの重要な特徴は、その状態の特異性でもあります。 このようなオブジェクトには、1)結果がない、2)結果があるという2つの状態があります。 さらに、遷移は、最初の状態から2番目の状態にのみ可能です。 結果が得られると、それが存在しない状態になったり、別の結果を持つ状態になったりすることはできなくなります。
このオブジェクトの次の簡単なインターフェイスを考えてみましょう。
function Deferred () // constructor function on (callback) function resolve (result)
on
メソッドはコールバックを受け入れます。 コールバックは、結果が利用可能になるとすぐに呼び出され、パラメーターとして渡されます。 これは、パラメーターとして渡される通常のコールバックとの完全な類推です。 コールバック登録の時点で、オブジェクトは結果のある状態と結果のない状態にある場合があります。 まだ結果がない場合、コールバックは到着時に呼び出されます。 結果がすでに存在する場合、コールバックはすぐに呼び出されます。 どちらの場合も、コールバックは1回呼び出され、結果を取得します。
resolve
メソッドを使用すると、オブジェクトを結果のある状態に変換(リゾルバー)し、この結果を指定できます。 このメソッドはべき等です 。つまり、
resolve
呼び出しを繰り返してもオブジェクトは変更されません。 結果のある状態に遷移すると、登録されたすべてのコールバックが呼び出され、
resolve
呼び出し後に登録されるすべてのコールバックがすぐに呼び出されます。 どちらの場合(
resolve
呼び出しの前後の登録)でも、オブジェクトがそれを保存するという事実のために、コールバックは結果を受け取ります。
この動作を持つオブジェクトは、 遅延 ( promiseおよびfutureとも呼ばれます )と呼ばれます。 単純なコールバックよりも、いくつかの利点があります。
1.結果からの非同期関数の抽象化:各非同期関数はコールバックパラメーターを提供する必要がなくなりました。 結果のサブスクリプションはクライアントコードに残ります。 たとえば、結果が不要な場合は、結果をサブスクライブする必要はありません(コールバックとしてnoop関数を渡すのと似ています)。 非同期関数のインターフェイスはよりクリーンになります。重要な入力パラメーターのみを持ち、不特定数のパラメーター、オプションパラメーターなどを持つ関数をより自信を持って使用することが可能になります。
2.結果の状態からの抽象化:コードのクライアントは、結果の現在の状態を確認する必要がなく、単にハンドラーに署名するだけで、結果が到着したかどうかを考えません。
3.複数のサブスクリプションの可能性:複数のハンドラーに署名することができ、結果を受信するとそれらすべてが呼び出されます。 コールバックスキームでは、たとえば、関数のグループを呼び出す関数を作成する必要があります。
4.遅延オブジェクトの「代数」など、いくつかの追加のアメニティ。これにより、オブジェクト間の関係を判断したり、チェーンで実行したり、そのようなオブジェクトのグループを正常に完了したりできます。
次の例を考えてみましょう。 非同期関数
getData(id, onSuccess)
があるとします。これは、2つのパラメーターを受け取ります:受信する要素のidと、結果を取得するためのコールバックです。 その使用の典型的なコードは次のようになります。
getData(id, function (item) { // do some actions with item });
Deferred
を使用してこれを書き換えます。 この関数にはシグネチャ
getData(id)
あり、次のように使用されます。
getData(id).on(function (item) { // do some actions with item });
この場合、コードはそれほど複雑にならず、アプローチは単純に変更されています。 結果は、遅延として関数の戻り値に渡されます。 ただし、後で明らかになるように、より複雑なケースでは、deferredを使用するとコードの可読性が向上します。
エラー処理
このようなオブジェクトを使用する場合のエラー処理の問題は合理的です。 同期コードでは、例外メカニズムが広く使用されています。これにより、エラーが発生した場合に「ローカル」コードを大幅に複雑化せずにすべてのエラーをキャッチして処理できるため、プログラマがくしゃみごとにチェックを書く必要がなくなります。
非同期コード(およびコールバックのある回路)で例外を使用するのは困難です。例外は結果のように非同期に到着するため、
try
非同期関数の呼び出しをフレーミングしてもキャッチできないためです。 エラーを考慮すると、実際には、これは関数の別の結果(負の結果と言うこともできます)であり、エラーのオブジェクト(例外)は戻り値として機能します。
そのような結果は、成功と同様に、コールバックとして実装されます( errback 、 エラーからの単語の再生およびbackとも呼ばれます )。
Deferred
トレーニングオブジェクトを強化して、成功と失敗に別々のサブスクリプションを提供できるようにします。つまり、
on
メソッドを再処理し
on
resolve
ます。
function on (state, callback)
最初のパラメーターとして、
E_SUCCESS
、
E_ERROR
などの2つの値を持つ列挙型の値を渡すことができます。 読みやすくするために、例では単純な文字列値「success」、「error」を使用します。 また、このメソッドを強化し、
Deferred
オブジェクト自体を返すようにします。 これにより、サブスクリプションチェーンの使用が許可されます(JavaScriptに非常に固有の手法)。
resolve
方法はそれに応じて変更されます。
function resolve (state, result)
最初のパラメーターとして、状態は
Deferred
(エラー、成功)オブジェクトに渡され、2番目は結果です。 状態ルールは、そのような変更されたオブジェクトにも適用されます。結果のある状態への遷移後、オブジェクトはその状態を別のものに変更できません。 これは、たとえば、オブジェクトが成功状態になった場合、エラーに対して登録されたすべてのハンドラーが機能しないことを意味します。
そのため、
getData
関数
getData
何らかのエラー(データなし、誤った入力、失敗など)で失敗するようにします。
コードは次の形式を取ります。
getData(id) .on('success', function (item) { // do some actions with item }) .on('error', function (err_code) { // deal with error });
より現実的な例を考えてみましょう。つまり、標準のNode.jsから標準のfs.readFileメソッドを取得します。 このメソッドは、ファイルの読み取りに使用されます。 記事の冒頭で、ほぼすべての関数を同期スタイルまたは非同期スタイルで記述できることが述べられました。 標準のNode.jsライブラリでは、ファイルAPIは両方のスタイルで定義され、各関数には独自の同期対応物があります。
たとえば、非同期バージョンのreadFileを使用し、Deferredを使用するように調整します。
function readFileDeferred (filename, options) { var result = new Deferred; fs.readFile(filename, options, function (err, data) { if (err) { result.resolve('error', err); } else { result.resolve('success', data); } }); return result; }
このような関数は、成功とエラーの関数を別々に登録できるため、使用するのが多少便利です。
説明されている機能は、ほとんどの場合に十分ですが、延期にはさらに可能性があります。これについては以下で説明します。
高度な遅延オブジェクト機能
1.結果オプションの数に制限はありません。 この例では、
Deferred
オブジェクトを使用して、成功とエラーの2つの結果が得られました。 他の(カスタム)オプションの使用を妨げるものは何もありません。 幸いなことに、状態として文字列値を使用したため、列挙型を変更せずに結果のセットを定義できます。
2.結果のすべてのオプションをサブスクライブする機能。 これは、すべての種類の一般化されたハンドラーに使用できます(これは、段落1と組み合わせて最も理にかなっています)。
3.サブオブジェクトプロミスを作成します。
Deferred
オブジェクトのインターフェイスから、クライアントコードが
resolve
メソッドにアクセスできることが
Deferred
ますが、実際には、サブスクライブする機能のみが必要です。 この改善の本質は
promise
メソッドの導入です
promise
メソッドは
Deferred
オブジェクトの「サブセット」を返し、そこからサブスクリプションのみを利用できますが、結果は設定しません。
4.延期された状態から別の状態への状態の転送。オプションで、結果を変換します。 これは、マルチレベルの呼び出しに非常に役立ちます。
5.他の遅延のセットの結果に依存する、遅延の作成。 この改善の本質は、非同期操作のグループの結果をサブスクライブすることです。
2つのファイルを読み取り、両方で何か面白いことをする必要があるとします。 これを行うには、
readFileDeferred
関数を使用します。
var r1 = readFileDeferred('./session.data'), r2 = readFileDeferred('./data/user.data'); var r3 = Deferred.all(r1, r2); r3.on('success', function (session, user) { session = JSON.parse(session); user = JSON.parse(user); console.log('All data recieved', session, user); }).on('error', function (err_code) { console.error('Error occured', err_code); });
Deferred.all
は新しい
Deferred
オブジェクトを作成し、渡されたすべての引数がこの状態に渡されると成功状態に切り替わります。 そうすることで、すべての遅延の結果も引数として受け取ります。 少なくとも1つの引数がエラー状態になると、
Deferred.all
の結果もこの状態になり、結果としてエラー状態になった引数の結果を取得します。
JavaScriptの遅延機能
JavaScriptにはマルチスレッドが存在しないことに注意してください。 コールバックが
setInterval
/
setTimeout
またはイベントによって設定された場合、現在のコードの実行を中断したり、並行して実行したりすることはできません。 これは、非同期関数の結果が即座に到着した場合でも、現在のコードの実行の完了後にのみ受信されることを意味します。
JavaScriptでは、関数は任意の数のパラメーターと任意のコンテキストで呼び出すことができます。 これにより、コールバックに必要な数のパラメーターを転送できます。 たとえば、非同期関数が値のペア
(X, Y)
返す場合、それらは2つのフィールドを持つオブジェクト、または2つの値を持つリスト(タプルの即興の類似物)として送信できます。または、この目的でコールバックの最初の2つの引数を使用できます。
この場合のコールバック呼び出しは、次の形式を取ることができます。
callback.call(this, X, Y);
JavaScriptはリンクを使用し、メモリの解放はガベージコレクターによって制御されます。 遅延オブジェクトは、非同期関数の内部(結果の到着を通知するため)と外部(結果を取得するための両方)の両方で必要です。メモリを操作するより厳格なモデルを持つ言語では、そのようなオブジェクトの有効期間の正しい処理に注意する必要があります。
既存の据え置き
1. jQueryには、
$.Deferred
オブジェクト( ドキュメント )があります。 成功、エラー、進行通知へのサブスクリプションもサポートされています。結果が到着する前に生成される中間イベント。 状態を別のDeferred(
then
メソッド)に転送したり、Deferredリストの結果(
$.when
)でDeferredを登録したり、
promise
を作成したりできます。
すべてのライブラリajaxメソッドは、そのようなオブジェクトの約束を返します。
2. qライブラリは遅延オブジェクトを実装します。非同期関数のチェーンを作成することが可能で、遅延リストの結果によって遅延を登録できます。
3. async.jsライブラリを使用すると、非同期呼び出しでフィルター/マップ/削減を使用し、非同期呼び出しのチェーンとグループを作成できます。
4. when.jsライブラリでは、deferredの使用も許可されます。
5. Dojo Toolkitには、Deferredオブジェクト( ドキュメント )が含まれています。
6. Pythonの兄弟言語では、イベント駆動型のTwistedフレームワークにDeferredオブジェクト( documentation )があります。 この実装は非常に古く、結果の遅延というアイデアの祖先の権利を主張する場合があります。
成功、エラー、両方の結果のサブスクリプションをサポートします。 オブジェクトを一時停止できます。
7. Deferredに興味があるため、このオブジェクトの独自のバージョンを作成しました ( ドキュメント 、 ソースコード 、 テスト )。 この記事で説明されている多くの機能がサポートされています。
それだけです、ご清聴ありがとうございました。