JavaScriptのキャンセル可能な非同期関数


(この章では、ジェネレータを使用して、呼び出しを重複して呼び出すようにしますasync 関数.Check out this gist for the final approach または詳細を学ぶために読んでください!🎓)
JavaScriptは恐ろしい非同期呼び出しのツイート迷路です.私たちはすべてこのようなコードを書いているが、このポストでは、私は約async and await . これらはキーワードですwidely supported そして、あなたがそのコードをはるかに読みやすいものに移行するのを手伝ってください.📖👀
そして、最も重要なことに、私は重要な落とし穴をカバーします.🏑💥
例から始めましょう.この関数はいくつかのコンテンツを取得し、画面に表示し、注意を引く前に数秒待ちます.
function fetchAndFlash(page) {
  const jsonPromise = fetch('/api/info?p=' + page)
      .then((response) => response.json());
  jsonPromise.then((json) => {
    infoNode.innerHTML = json.html;

    setTimeout(() => {
      flashForAttention(infoNode);
    }, 5000);
  });
}
今、我々はこれで書き直すことができますasync and await このようにコールバックがない.
async function fetchAndFlash(page) {
  const response = await fetch('/api/info?p=' + page);
  const json = await response.json();
  infoNode.innerHTML = json.html;

  // a bit awkward, but you can make this a helper method
  await new Promise((resolve) => setTimeout(resolve, 5000));

  flashForAttention(infoNode);
}
それはより良いですか?それは飛び跳ねて、一番上にステップを見るのが簡単です:資源をフェッチして、JSONにそれを変えて、ページに書いて、5秒待って、もう一つの方法を呼び出してください.🔜

それはトラップです!


しかし、読者を混乱させることができる何かが、ここにあります.これは、「一度に」実行される通常の関数ではありませんawait , 基本的にはブラウザのイベントループに移ります.⚡🤖
別の方法を置くために:あなたが使用しているコードを読んでいるとしましょうfetchAndFlash() . この記事のタイトルを読んでいなければ、このコードを実行したらどうなるでしょうか?
fetchAndFlash('page1');
fetchAndFlash('page2');
あなたは、1つが他の後に起こると予想するかもしれません、あるいは、それはもう一方をキャンセルするでしょう.それは、両方とも平行に多かれ少なかれ実行される場合ではありません(JavaScriptが待つ間、ブロックすることができないので)、どちらの命令も終えてください、そして、どんなHTMLがあなたのページに終わるかは、わかりません.⚠️

明確に言えば、このメソッドのコールバックベースのバージョンは全く同じ問題を持っていましたが、それは非常に不快な種類の方法でより明白でした.使用するコードの近代化async and await , 我々はそれをより曖昧にする.😕
この問題を解決するためにいくつかの異なるアプローチをカバーしましょう.ストラップイン!🎢

チェーン1アプローチ:チェーン


あなたが電話している方法と理由によってasync 方法は、それを次々に'チェーン'することができるかもしれません.Clickイベントを処理しているとしましょう.
let p = Promise.resolve(true);
loadButton.onclick = () => {
  const pageToLoad = pageToLoadInput.value;
  // wait for previous task to finish before doing more work
  p = p.then(() => fetchAndFlash(pageToLoad));
};
クリックするたびに別のタスクをチェーンに追加します.ヘルパー関数でこれを一般化することもできます.
// makes any function a chainable function
function makeChainable(fn) {
  let p = Promise.resolve(true);
  return (...args) => {
    p = p.then(() => fn(...args));
    return p;
  };
}
const fetchAndFlashChain = makeChainable(fetchAndFlash);
今、あなたはただ呼び出すことができますfetchAndFlashChain() そして、他のどんな呼び出しの後も順番に起こるでしょうfetchAndFlashChain() . 🔗
しかし、私たちが前の操作をキャンセルしたいならば、それはこのブログ柱の提案でありません?ユーザーは、別の負荷ボタンをクリックしたので、おそらく前のことを気にしない.🙅

アプローチは、2をチェックします:バリアチェック


インサイドモダンfetchAndFlash() , 我々はawait キーワードは3回、2つの理由のためだけに:
  • ネットワークフェッチを行うには
  • 5秒待ってからフラッシュする
  • これらのポイントの後、私たちは立ち止まって、尋ねることができました🤔💭
    私たちは、それぞれの異なった操作をマークすることによってこれをすることができますa nonce . これは、ローカルおよびグローバルに格納されたユニークなオブジェクトを作成し、ローカルバージョンから別の操作が開始されたため、グローバルバージョンが分岐するかどうかを確認します.
    更新はこちらfetchAndFlash() メソッド:
    let globalFetchAndFlashNonce;
    async function fetchAndFlash(page) {
      const localNonce = globalFetchAndFlashNonce = new Object();
    
      const response = await fetch('/api/info?p=' + page);
      const json = await response.json();
      // IMMEDIATELY check
      if (localNonce !== globalFetchAndFlashNonce) { return; }
    
      infoNode.innerHTML = json.html;
    
      await new Promise((resolve) => setTimeout(resolve, 5000));
      // IMMEDIATELY check
      if (localNonce !== globalFetchAndFlashNonce) { return; }
    
      flashForAttention(infoNode);
    }
    
    これはうまくいきますが、少し口があいます.また、一般化するのは簡単ではありませんし、どこにでもチェックを追加することを忘れないでください!
    使い方は一つありますがgenerators 私たちのために一般化する.

    背景:発電機


    await それが我々のケースで終わるのを待つことまで、ネットワーク・リクエストかちょうどタイムアウトを待っているだけであるまで、Defers実行は発生します.
    混乱?それは速い再ハッシュの価値があります:
    function* myGenerator() {
      const finalOut = 300;
      yield 1;
      yield 20;
      yield finalOut;
    }
    for (const x of myGenerator()) {
      console.info(x);
    }
    // or, slightly longer (but exactly the same output)
    const iterator = myGenerator();
    for (;;) {
      const next = iterator.next();
      if (next.done) {
        break;
      }
      console.info(next.value);
    }
    
    このプログラムは、両方のバージョンでは、1、20と300を印刷します.おもしろいことは、私が好きなことは何でもできるfor ループを含むbreak 早く、すべての内部の状態myGenerator 私が宣言する同じどんな変数でも、そして、私がどこにいるかについています.
    ここでは表示されませんが、コードを生成するコードは.next() 反復子の関数は、変数を使って再開することもできます.どのようにすぐに表示されます.
    我々はこれらの部品を一緒に使用することができますだけでいくつかのタスクでは、我々が停止することを決定する場合は、作業を続行しないと、いくつかの出力で実行を再開します.HMMは、我々の問題に絶好です!✅

    解決策🎉


    書き換えましょうfetchAndFlash() 最後まで.私たちは文字通り関数型を変更し、スワップawait with yield : 呼び出し元は、次の方法を見ることができます.
    function* fetchAndFlash(page) {
      const response = yield fetch('/api/info?p=' + page);
      const json = yield response.json();
    
      infoNode.innerHTML = json.html;
    
      yield new Promise((resolve) => setTimeout(resolve, 5000));
    
      flashForAttention(infoNode);
    }
    
    このコードは本当に意味をなさない.それぞれを生み出す点Promise 現在、このジェネレータを呼び出すいくつかの関数はawait 我々のために、nonceをチェックすることを含むこと.あなたは今ちょうどあなたが使用する必要がある何かを待つのを待つときは、これらの行を挿入する気にする必要はありませんyield .
    そして、最も重要なのは、このメソッドは、現在の発電機ではないasync 関数await キーワードは実際にエラーです.これは、正しいコードを書くことを保証するために絶対的な最善の方法です!🚨
    我々が必要とするその機能は何ですか?さて、それはこのポストの本当の魔法です:
    function makeSingle(generator) {
      let globalNonce;
      return async function(...args) {
        const localNonce = globalNonce = new Object();
    
        const iter = generator(...args);
        let resumeValue;
        for (;;) {
          const n = iter.next(resumeValue);
          if (n.done) {
            return n.value;  // final return value of passed generator
          }
    
          // whatever the generator yielded, _now_ run await on it
          resumeValue = await n.value;
          if (localNonce !== globalNonce) {
            return;  // a new call was made
          }
          // next loop, we give resumeValue back to the generator
        }
      };
    }
    
    それは魔法です、しかし、うまくいけば、それも意味をなします.渡されたジェネレータを呼び出し、イテレータを取得します.我々await すべての値では、生成されるまでのネットワーク応答のように、結果値で再開します.重要なことに、これは、各非同期操作の後にグローバルvsローカルローカルACCEをチェックする私たちの能力を一般化します.
    拡張子:個々の呼び出しがキャンセルされたかどうかを知るのに便利です.にsample gist Aを返すSymbol , あなたが比較できるユニークなオブジェクト.
    最後に、我々は実際に使用するmakeSingle そして、他の人のために私たちのジェネレータを包んでください、それで、現在、それは通常のasyncメソッドと同じように動きます:
    // replaces fetchAndFlash so all callers use it as an async method
    fetchAndFlash = makeSingle(fetchAndFlash);
    
    // ... later, call it
    loadButton.onclick = () => {
      const pageToLoad = pageToLoadInput.value;
      fetchAndFlash(pageToLoad);  // will cancel previous work
    };
    
    ああ!今、あなたは呼び出すことができますfetchAndFlash() どこからでも好きなように、任意の前の呼び出しは、できるだけ早くキャンセルすることを知っている.

    アグレッシブフェッチ


    熱心な人々は、私が上でカバーしたものがちょうどメソッドをキャンセルするが、どんな飛行中の仕事でも中止しない点に注意するかもしれません.話していますfetch , どちらが多少支持されているかto abort the network request . async関数が言っているなら、これはあなたのユーザー帯域幅を節約するかもしれません、本当に大きなファイルをダウンロードします.

    完了


    あなたがこれまで読んだならば、あなたはうまくいけば、JavaScriptが動く方法についてもう少し考えました.
    JSは非同期の作業を行う必要があるときにブロックすることはできません.複数のメソッドを呼び出すことができます.また、チェインのどちらかに対処するための戦略を持つこともできます.
    読書ありがとう!👋