EcmaScriptの非同期およびイベントスケジューラー
Promise(約束)の概念は、最新のEcmaScriptの重要な要素の1つです。 Promiseを使用すると、非同期アクションをチェーンに編成することで非同期アクションの一貫した実行を保証でき、さらにエラートラップも提供できます。 async / awaitステートメントの最新の構文も技術的にはPromiseに基づいており、単なる構文上の砂糖です。
そのすべての豊富な機能について、Promiseには1つの欠点があります-実行中のアクションをキャンセルする機能をサポートしていません。 この制限を回避する方法を理解するには、Promiseはそれらの単なるラッパーであるため、非同期アクションがEcmaScriptでどのように発生し、機能するかを考慮する必要があります。
EcmaScriptエンジンは、V8であれChakraであれ、シングルスレッドであり、一度に1つのアクションのみを許可します。 ブラウザー環境では、かなり最新のエンジンがWebWorkersテクノロジーをサポートしており、Node.jsで個別の子プロセスを作成できます。これにより、コード実行が並列化されます。 ただし、作成された実行スレッドは、メッセージを介してのみ作成したスレッドと情報を交換できる独立したプロセスであるため、これ自体はマルチスレッドモデルではありません。
代わりに、従来のEcmaScriptは多重化モデルに基づいています。複数のアクションを並行して実行するために、アクションは小さなフラグメントに分割され、各フラグメントは比較的迅速に実行され、実行フローをブロックしません。 このようなフラグメントを混合することにより、それらに関連付けられたアクションは実際に並行して実行されます。
たとえば、Webページのビジュアルインターフェイス(UI)のレンダリングなどのユーザーコードとホスト環境関数は同じスレッドで実行されるため、ユーザーコードの長いループまたは無限ループはWebのレンダリングの一時停止につながります。ページとそのフリーズ。 特定のコードフラグメントが実行される時間間隔を分離するには、イベントスケジューラ、つまりイベントループを使用します。 実行可能フラグメントはイベントループにどのように表示されますか?
通常のクライアントコードは、条件、ループ、および関数呼び出しを伴う実行スレッドで構成される一連のアクションのみを実行します。 遅延実行を実行するには、ホスト環境にクライアントコールバック関数を登録する必要があります。
ブラウザー環境では、これは通常、タイマー、イベント、およびリソースの非同期要求の3つの可能性のいずれかになります。 タイマーは、時間後(
setTimeout
)、イベントスケジューラの最初の空きスロット(
setImmediate
)、またはWebページのレンダリングプロセス(
requestAnimationFrame
)でも関数呼び出しを提供します。 イベントは、原則としてDOMモデルで行われたアクションに対する反応であり、ユーザー(イベント:ボタンをクリック)とUI要素を表示する内部プロセス(イベント:スタイル再カウント完了)の両方によってトリガーできます。 リソース要求は別のカテゴリに配置されますが、実際にはイベントに関連していますが、唯一の違いは元のイニシエーターがクライアントコードそのものであることです。
これは、次の図に明確に示されています。
非同期アクションラッパー
さらに、上記の非同期アクションがどのようにPromiseに変わるかを考慮することが重要です。 Promiseをキャンセルするためのアスペクトの最大数に対処するために、次のコードはタイマー、DOMモデルイベント、およびそれらを結合する任意のクライアントコードの使用を組み合わせます。 この例では、CSV形式で大量のデータを返すAJAXリクエストを実行し、メインスレッドがフリーズするのを防ぐために潜在的に低速な行単位の関数で処理します。
function fetchInformation() { function parseRow(rawText) { /* Some function for row parsing which works very slow */ } const xhrPromise = new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', '.../some.csv'); // API endpoint URL with some big CSV database xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(String(xhr.response)); } else { reject(new Error(xhr.status)); } }; xhr.onerror = () => { reject(new Error(xhr.status)); }; xhr.send(); }); const delayImpl = window.setImmediate ? setImmediate : requestAnimationFrame; const delay = () => new Promise(resolve => delayImpl(resolve)) const parsePromise = (response) => new Promise((resolve, reject) => { let flowPromise = Promise.resolve(); let lastDemileterIdx = 0; let result = []; while(lastDemileterIdx >= 0) { const newIdx = response.indexOf('\n', lastDemileterIdx); const row = response.substring( lastDemileterIdx, (newIdx > -1 ? newIdx - lastDemileterIdx : Infinity) ); flowPromise = flowPromise.then(() => { result.push(parseRow(row)); return delay(); }); lastDemileterIdx = newIdx; } flowPromise.then(resolve, reject); }); return xhrPromise.then(parsePromise); }
AJAXリクエストの成功またはエラー完了はDOMモデルのイベントとして使用され、タイマーは大量のデータの順次バッチ処理を提供し、UIストリームに作業時間を提供します。 外部の観点から見ると、このようなPromiseはモノリシックな要素であり、その最後で適切な形式の処理済みデータベースが呼び出し元に利用可能であること、または実行中に障害が発生した場合のエラーの説明です。
発信者の観点からは、このようなPromise全体をキャンセルできると便利です。 たとえば、ユーザーがこのデータを表示するために必要な視覚要素を閉じた場合。 ただし、内部構造の観点から見ると、Promiseは同期アクションと非同期アクションのセットであり、その一部はすでに起動されて完了している場合があります。 このシーケンスは任意のクライアントコードによって決定されるため、緊急完了手順も手動で記述する必要があります。
キャンセルされた約束の実装
ループなどの同期コードの中断は原則的に発生しないことに注意してください。コードが既に実行されている場合(およびEcmaScriptエンジンがシングルスレッドである場合)、それを中断する他のコードは実行できません。 したがって、タイマー、イベント、および外部リソースへの呼び出しなど、完了が必要なのは上記の真の非同期アクションのみです。
タイマー設定関数には、それらをキャンセルするための
clearTimeout
、
clearImmediate
、
cancelAnimationFrame
2つの操作が
cancelAnimationFrame
ます。 DOMモデルのイベントの場合、対応するコールバック関数から単にサブスクライブを解除します。 また、タイマーの場合は、より簡単なアプローチを使用できます-手動の
isCancelled
フラグを持つPromiseオブジェクトでそれらを事前にラップします。 タイマーの期限が切れた後、Promiseをキャンセルする必要がある場合、コールバック関数は単に実行されません。 この場合、タイマーはスケジューラに残りますが、キャンセルした場合、終了後に何も起こりません。
外部リソースにアクセスする場合、状況はより複雑です。いずれの場合も、対応するイベントのサブスクライブを解除することで操作の結果を無視できますが、操作自体を中断できるとは限りません。 Promiseの実行ロジックの観点からは、これは重要ではないかもしれませんが、中断されない操作は不必要なリソースを消費します。
特に、AJAXリクエストを実行するための従来の
XMLHttpRequest
を置き換えるように設計され、追加のラッパーを必要とせずにすぐにPromiseオブジェクトを返す
fetch
メソッドでは、リクエストをキャンセルできません。 このため、HTTPリクエストを実際にキャンセルするには、
XMLHttpRequest
オブジェクトで
abort
メソッドを使用する必要があります。
結果のPromiseキャンセルサポートコードは次のようになります。 わかりやすくするために、変更されたコードのみが表示され、古いコードは省略記号付きのコメントに置き換えられます。
function fetchInformation() { /* ... */ let isCancelled = false; let xhrAbort; const xhrPromise = new Promise((resolve, reject) => { /* ... */ xhrAbort = xhr.abort.bind(xhr); }); const delayImpl = window.setImmediate ? setImmediate : requestAnimationFrame; const delay = () => new Promise((resolve, reject) => delayImpl(() => (!isCancelled ? resolve(): reject(new Error('Cancelled')))) ); /* ... */ const promise = xhrPromise.then(parsePromise); promise.cancel = () => { try { xhrAbort(); } catch(err) {}; isCancelled = true; } return promise; }
PromiseはEcmaScriptの観点から見ると通常のオブジェクトなので、
cancel
メソッドを簡単に追加できます。 また、結果のPromiseオブジェクトが1つだけ外部環境に返されるため、
cancel
メソッドがそのオブジェクトに対してのみ追加され、すべての内部ロジックが生成関数の現在の字句ブロックにカプセル化されます。
まとめ
EcmaScriptでキャンセルされたPromiseを実装することは、内部に連続した呼び出しの非自明なロジックがある非同期チェーンに対しても簡単に実行できる比較的単純なタスクです:オブジェクトにキャンセルフラグを保存し、関数を生成するコンテキストをアクティブにします。 キャンセルは、Promiseがエラーで中断されてサードパーティの効果が実行されない場合の表面的なもの、または開始されたすべての非同期操作(タイマー、外部リソースへの呼び出しなど)が実際にキャンセルされた場合の深さです
キャンセルされたPromiseの重要な側面は、キャンセル操作を完全に手動で実装する必要があることです。 たとえば、独自のPromiseクラスを実装することで、自動的に達成することはできません。 理論的には、仮想マシンでコードを実行することで問題を解決できます。仮想マシンでは、Promise初期化スタックで開始されるすべての非同期アクションと依存するブランチが記録されますが、これは実際にはかなり重要なタスクであり、実際にはあまり役に立ちません。
したがって、EcmaScriptでキャンセルされたPromiseは、論理アクションのカプセル化されたチェーンであるPromiseエフェクトを中断およびキャンセルできるインターフェイス規則にすぎません。 一般的な場合、キャンセルの概念は存在しません。
編集者から
トピックに関するNetologyコース:
- 職業「 フロントエンド開発者 」。
- 職業「 ウェブ開発者 」。
- JavaScriptベーシックコースオンラインプログラム。
- オンラインプログラム「 Node、AngularJS、MongoDB:本格的なWebアプリケーションの開発 」;
- オンラインプログラム「 ブラウザのJavaScript:インタラクティブなWebページの作成 」