Node.js v15に実装されたAbortController

UI開発者 加藤

この記事はミツエーリンクス Advent Calendar 2020 - Adventarの14日目の記事です。

少し前にNode.jsのv15がリリースされました。v15にはAbortControllerの実装が追加されています。

AbortControllerは簡単に言うとPromiseなどの非同期処理を中断させるために実装されたインターフェースです。Node.jsだけではなくWeb APIにも存在しており、この度Node.jsに実装されたAbortControllerはWeb APIをベースにしています(ただしExperimental扱いです)

(12/15追記:12/9にリリースされたNode.js 15.4.0Experimentalではなくなりました。)

今回はAbortControllerをどのように使うのかをご紹介したいとおもいます。

AbortControllerの使い方

ここからはNode.jsではなく、Web APIとしてのAbortControllerについて解説していきます。
繰り返しになりますが、AbortControllerとは非同期処理を途中で止めるための仕組みです。

分かりやすいユースケースとして、大きなファイルをダウンロード(fetch)しようとしている時のことを考えてみましょう。

ユーザーがファイルをダウンロードするボタンをタップすると、プログレスバーが表示されて、ダウンロードが開始します。しかし、ユーザーが思っていたよりファイルサイズが大きく「ダウンロードを中止したい!」となったとしても、fetchを途中で止める手段はありません。結局、ユーザーは画面のリロードをするしかダウンロードを止める手段がありません。これはあまり良くないUXでしょう。

このような問題を解決するのがAbortControllerです。基本的な使い方は以下のようになります。


const cancelButton = document.getElementById('btn-cancel');

function downloadTooBigFile() {
    const abortController = new AbortController();
    const {signal} = abortController;

    cancelButton.addEventListener('click', function () {
        abortController.abort();
    }, { once: true });

    return new Promise(function (resolve, reject) {

        // 巨大なファイルをダウンロード後、resolveする処理

        signal.addEventListener('abort', function () {
            reject();
        }, { once: true });
    });
}

AbortControllerAbortSignalクラスのインスタンスをプロパティに持っています。

AbortSignalabortイベントとabortedプロパティのみを持つシンプルなクラスですが、fetch APIと組み合わせることができます。

AbortSignalとfetch

fetch APIのRequestAbortSignalをオプションとして受け取ることができます。具体的にはfetchの第二引数にAbortSignalを渡すことで、外からfetchを中断させることができます。


(async function (){
    const cancelButton = document.getElementById('btn-cancel');
    const abortController = new AbortController();
    const {signal} = abortController;

    cancelButton.addEventListener('click', function() {
        abortController.abort();
    });

    try {
        await fetch('https://example.com', {signal});
    } catch(err) {
        console.error(err.name);
    }
})();

abortController.abortメソッドが実行され、AbortSignalabortイベントが発火すると、例外が発生しcatchブロックに処理が流れます。
1つのAbortSignalは複数のfetchに指定できるため、同時にたくさんのリクエストを管理したい場合に非常に有効です。

注意点として、一度中断されたAbortSignalに紐づいているfetchを再度実行することはできません。
中断したfetchをもう一度行いたい場合はAbortControllerのインスタンスも併せて生成しなおす必要があります。

ちなみに、ServiceWorkerでfetchイベントを制御している際にクライアント側でabortした場合のふるまいについては、GitHubにイシューが立てられ議論されているようですが、まだ明確な結論は出ていないようです。

AbortSignalとaddEventListener

AbortSignalEventTargetAddEventListenerOptionsとしても設定することができます。

設定したAbortSignalオブジェクトがabortされた時点でイベントリスナーは削除されます。(removeEventListenerと同等)


const cancelButton = document.getElementById('btn-cancel');
const abortController = new AbortController();
const {signal} = abortController;

window.addEventListener('scroll', function () {
    // スクロールごとの処理
}, {signal});

cancelButton.addEventListener('click', function() {
    abortController.abort();
});

removeEventListenerでは第二引数に渡したリスナー関数が無名関数の場合にはイベントリスナーを削除することができませんでしたが、AbortSignalを設定すれば、無名関数の場合でも削除できます。
また、複数のリスナー関数を一度に削除できるため、コード量を減らしてシンプルに書くことができるでしょう。

さいごに

1つのAbortControllerで複数のfetchEventを管理することは、それぞれの関係性を正しく理解していないと不具合を生む可能性があります。 しかし逆を言えば、あらかじめ仕様を明確にし、関係性を理解したうえで実装することができれば、複雑化しがちな処理の流れをより分かりやすく管理できるはずです!ぜひ、ご活用ください!