web-dev-qa-db-ja.com

非同期/待機フローを短絡する方法はありますか?

async function update() {
   var urls = await getCdnUrls();
   var metadata = await fetchMetaData(urls);
   var content = await fetchContent(metadata);
   await render(content);
   return;
}
//All the four functions return a promise. (getCdnUrls, fetchMetaData, fetchContent, render)

外部からシーケンスをいつでも中止したい場合はどうなりますか?

たとえば、fetchMetaDataが実行されているときに、コンポーネントをレンダリングする必要がなくなったことがわかり、残りの操作(fetchContentおよびrender)をキャンセルしたいとします。消費者が外部から中止/キャンセルする方法はありますか?

条件を待機するたびに確認することもできますが、これはこれを行うための洗練されていない方法のようです。現在の操作が完了するまで待機します。

40
sbr

私はこれについて話をしただけです-これは素敵なトピックですが、残念ながら、私が提案するソリューションはゲートウェイソリューションなので、あなたは本当に好きではありません。

仕様はあなたのために何をしますか

「ちょうどいい」キャンセルを取得することは実際には非常に困難です。人々はしばらくそのために取り組んでおり、非同期機能をブロックしないことが決定されました。

ECMAScriptコアでこれを解決しようとする2つの提案があります。

どちらの提案も、先週大幅に変更されたため、来年くらいに到着することを期待していません。提案はやや補完的であり、対立していません。

これをあなたの側から解決するためにあなたができること

キャンセルトークンは簡単に実装できます。残念ながら、本当にしたいキャンセル(別名 " 第3の状態 キャンセルは例外ではないキャンセル))は、現時点では非同期関数では不可能ですあなたはそれらがどのように実行されるかを制御しないので、あなたは2つのことをすることができます:

  • 代わりにコルーチンを使用してください- bluebird は、ジェネレーターとプロミスを使用したサウンドキャンセレーションで出荷されます。
  • 不完全なセマンティクスでトークンを実装する-これは実際にはかなり簡単なので、ここで実行しましょう

CancellationTokens

トークンはキャンセルを通知します:

class Token {
   constructor(fn) {
      this.isCancellationRequested = false; 
      this.onCancelled = []; // actions to execute when cancelled
      this.onCancelled.Push(() => this.isCancellationRequested = true);
      // expose a promise to the outside
      this.promise = new Promise(resolve => this.onCancelled.Push(resolve));
      // let the user add handlers
      fn(f => this.onCancelled.Push(f));
   }
   cancel() { this.onCancelled.forEach(x => x); }
}

これにより、次のようなことが可能になります。

async function update(token) {
   if(token.isCancellationRequested) return;
   var urls = await getCdnUrls();
   if(token.isCancellationRequested) return;
   var metadata = await fetchMetaData(urls);
   if(token.isCancellationRequested) return;
   var content = await fetchContent(metadata);
   if(token.isCancellationRequested) return;
   await render(content);
   return;
}

var token = new Token(); // don't ned any special handling here
update(token);
// ...
if(updateNotNeeded) token.cancel(); // will abort asynchronous actions

これは非常に醜い方法ですが、非同期関数がこれを認識しているのが最適ですが、(まだ)ではありません。

理想的には、すべての中間関数が認識され、キャンセル時にthrowになります(これも、3番目の状態を取得できないためです)。

async function update(token) {
   var urls = await getCdnUrls(token);
   var metadata = await fetchMetaData(urls, token);
   var content = await fetchContent(metadata, token);
   await render(content, token);
   return;
}

各関数はキャンセル対応であるため、実際の論理的なキャンセルを実行できます。getCdnUrlsはリクエストを中止してスローでき、fetchMetaDataは基になるリクエストを中止してスローできる、などです。

これは、ブラウザでgetCdnUrl AP​​Iを使用してXMLHttpRequest(単数形に注意)を記述する方法です。

function getCdnUrl(url, token) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    var p = new Promise((resolve, reject) => {
      xhr.onload = () => resolve(xhr);
      xhr.onerror = e => reject(new Error(e));
      token.promise.then(x => { 
        try { xhr.abort(); } catch(e) {}; // ignore abort errors
        reject(new Error("cancelled"));
      });
   });
   xhr.send();
   return p;
}

これは、コルーチンなしの非同期関数で取得できる限り近くなります。あまりきれいではありませんが、確かに使えます。

キャンセルが例外として扱われるのを避けたいことに注意してください。これは、キャンセル時に関数throwの場合、グローバルエラーハンドラprocess.on("unhandledRejection", e => ...などでこれらのエラーをフィルタリングする必要があることを意味します。

26

TypeScript + Bluebird + cancelable-awaiter を使用して、必要なものを取得できます。

すべてのエビデンスがキャンセルトークンを指し示しているため ECMAScriptに到達していない 、キャンセルの最良の解決策は @ BenjaminGruenbaum によって言及されているbluebird実装であると思いますが、使用方法はわかりますコルーチンとジェネレーターは少し不器用で目に不安です。

TypeScriptを使用しているため、es5およびes3ターゲットのasync/await構文をサポートしているため、デフォルトの__awaiterヘルパーをbluebirdのキャンセルをサポートするヘルパーに置き換えるシンプルなモジュールを作成しました: https: //www.npmjs.com/package/cancelable-awaiter

4
Itay

残念ながら、いいえ、デフォルトの非同期/待機動作の実行フローを制御することはできません。問題自体が不可能であるという意味ではなく、アプローチを少し変更する必要があるということです。

まず、すべての非同期行をチェックでラップするという提案は有効な解決策です。そのような機能を持つ場所が2つしかない場合は、何も問題はありません。

このパターンをかなり頻繁に使用したい場合、おそらく最良の解決策は ジェネレーターに切り替える です:それほど普及していませんが、各ステップの動作を定義できるため、キャンセルを追加するのが最も簡単です。ジェネレーターは かなり強力 ですが、前述したように、ランナー関数が必要であり、非同期/待機ほど単純ではありません。

別のアプローチは、作成することです キャンセル可能なトークンのパターン –この機能を実装したい関数が入力されるオブジェクトを作成します:

async function updateUser(token) {
  let cancelled = false;

  // we don't reject, since we don't have access to
  // the returned promise
  // so we just don't call other functions, and reject
  // in the end
  token.cancel = () => {
    cancelled = true;
  };

  const data = await wrapWithCancel(fetchData)();
  const userData = await wrapWithCancel(updateUserData)(data);
  const userAddress = await wrapWithCancel(updateUserAddress)(userData);
  const marketingData = await wrapWithCancel(updateMarketingData)(userAddress);

  // because we've wrapped all functions, in case of cancellations
  // we'll just fall through to this point, without calling any of
  // actual functions. We also can't reject by ourselves, since
  // we don't have control over returned promise
  if (cancelled) {
    throw { reason: 'cancelled' };
  }

  return marketingData;

  function wrapWithCancel(fn) {
    return data => {
      if (!cancelled) {
        return fn(data);
      }
    }
  }
}

const token = {};
const promise = updateUser(token);
// wait some time...
token.cancel(); // user will be updated any way

私はキャンセルとジェネレーターの両方について記事を書きました:

要約すると、canncellationをサポートするためにいくつかの追加の作業を行う必要があります。それをアプリケーションのファーストクラスシチズンにしたい場合は、ジェネレーターを使用する必要があります。

3
Bloomca

新しいプロミスを作成する必要があるsimpleの例を次に示します。

let resp = await new Promise(function(resolve, reject) {
    timeout(5000).then(() => resolve('Promise RESOLVED !'), reject);
    $('#btn').click(() => resolve('Promise CANCELED !'));
});

デモはこちら codepen をご覧ください

0
538ROMEO