JavaScriptのAbortControllerを使用して、fetchリクエスト、タイマーなどの非同期操作を効果的にキャンセルする方法を学び、よりクリーンで高性能なコードを確保します。
JavaScript AbortController: 非同期操作のキャンセルをマスターする
現代のWeb開発では、非同期操作は至る所に存在します。APIからのデータの取得、タイマーの設定、ユーザーインタラクションの処理は、独立して実行され、潜在的に長期間にわたって実行されるコードを伴うことがよくあります。ただし、これらの操作が完了する前にキャンセルする必要があるシナリオがあります。ここで、JavaScriptのAbortController
インターフェースが登場します。これは、DOM操作やその他の非同期タスクにキャンセルリクエストを通知するためのクリーンで効率的な方法を提供します。
キャンセルが必要な理由を理解する
技術的な詳細に入る前に、非同期操作をキャンセルすることがなぜ重要なのかを理解しましょう。次の一般的なシナリオを考えてみてください。
- ユーザーナビゲーション: ユーザーが検索クエリを開始し、APIリクエストをトリガーします。リクエストが完了する前に別のページにすばやく移動した場合、元のリクエストは無関係になり、不要なネットワークトラフィックや潜在的な副作用を回避するためにキャンセルする必要があります。
- タイムアウト管理: 非同期操作にタイムアウトを設定します。タイムアウトが切れる前に操作が完了した場合は、冗長なコードの実行を防ぐためにタイムアウトをキャンセルする必要があります。
- コンポーネントのアンマウント: ReactやVue.jsなどのフロントエンドフレームワークでは、コンポーネントが非同期リクエストを行うことがよくあります。コンポーネントがアンマウントされると、そのコンポーネントに関連付けられている進行中のリクエストは、メモリリークやアンマウントされたコンポーネントの更新によって発生するエラーを回避するためにキャンセルする必要があります。
- リソース制約: リソースが制約された環境(モバイルデバイス、組み込みシステムなど)では、不要な操作をキャンセルすると、貴重なリソースを解放し、パフォーマンスを向上させることができます。たとえば、ユーザーがページのそのセクションをスクロールして過ぎた場合、大きな画像のダウンロードをキャンセルします。
AbortControllerとAbortSignalの紹介
AbortController
インターフェースは、非同期操作のキャンセルという問題を解決するために設計されています。これは、次の2つの主要なコンポーネントで構成されています。
- AbortController: このオブジェクトは、キャンセルシグナルを管理します。これには、キャンセルリクエストを通知するために使用される単一のメソッド
abort()
があります。 - AbortSignal: このオブジェクトは、操作を中止する必要があるシグナルを表します。これは
AbortController
に関連付けられており、キャンセル可能にする必要がある非同期操作に渡されます。
基本的な使用法: Fetchリクエストのキャンセル
fetch
リクエストをキャンセルする簡単な例から始めましょう。
const controller = new AbortController();
const signal = controller.signal;
fetch('https://api.example.com/data', { signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Data:', data);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
});
// To cancel the fetch request:
controller.abort();
説明:
AbortController
インスタンスを作成します。controller
から関連付けられたAbortSignal
を取得します。signal
をfetch
オプションに渡します。- リクエストをキャンセルする必要がある場合は、
controller.abort()
を呼び出します。 .catch()
ブロックで、エラーがAbortError
であるかどうかを確認します。そうである場合、リクエストがキャンセルされたことがわかります。
AbortErrorの処理
controller.abort()
が呼び出されると、fetch
リクエストはAbortError
で拒否されます。このエラーをコードで適切に処理することが重要です。そうしないと、未処理のPromiseリジェクションや予期しない動作につながる可能性があります。
エラー処理を含む、より堅牢な例を次に示します。
const controller = new AbortController();
const signal = controller.signal;
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data', { signal });
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
console.log('Data:', data);
return data;
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
return null; // Or throw the error to be handled further up
} else {
console.error('Fetch error:', error);
throw error; // Re-throw the error to be handled further up
}
}
}
fetchData();
// To cancel the fetch request:
controller.abort();
AbortErrorを処理するためのベストプラクティス:
- エラー名を確認する: 常に
error.name === 'AbortError'
かどうかを確認して、正しいエラータイプを処理していることを確認します。 - デフォルト値を返すか、再スローする: アプリケーションのロジックに応じて、デフォルト値(例:
null
)を返すか、コールスタックをさらに上に処理するためにエラーを再スローすることができます。 - リソースをクリーンアップする: 非同期操作が何らかのリソース(タイマー、イベントリスナーなど)を割り当てた場合は、
AbortError
ハンドラーでそれらをクリーンアップします。
AbortSignalを使用したタイマーのキャンセル
AbortSignal
は、setTimeout
またはsetInterval
で作成されたタイマーをキャンセルするためにも使用できます。組み込みのタイマー関数がAbortSignal
を直接サポートしていないため、これにはもう少し手作業が必要です。アボートシグナルをリッスンし、トリガーされたときにタイマーをクリアするカスタム関数を作成する必要があります。
function cancellableTimeout(callback, delay, signal) {
let timeoutId;
const timeoutPromise = new Promise((resolve, reject) => {
timeoutId = setTimeout(() => {
resolve(callback());
}, delay);
signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
reject(new Error('Timeout Aborted'));
});
});
return timeoutPromise;
}
const controller = new AbortController();
const signal = controller.signal;
cancellableTimeout(() => {
console.log('Timeout executed');
}, 2000, signal)
.then(() => console.log("Timeout finished successfully"))
.catch(err => console.log(err));
// To cancel the timeout:
controller.abort();
説明:
cancellableTimeout
関数は、コールバック、遅延、およびAbortSignal
を引数として取ります。setTimeout
を設定し、タイムアウトIDを保存します。abort
イベントをリッスンするAbortSignal
にイベントリスナーを追加します。abort
イベントがトリガーされると、イベントリスナーはタイムアウトをクリアし、プロミスを拒否します。
イベントリスナーのキャンセル
タイマーと同様に、AbortSignal
を使用してイベントリスナーをキャンセルできます。これは、アンマウントされているコンポーネントに関連付けられているイベントリスナーを削除する場合に特に役立ちます。
const controller = new AbortController();
const signal = controller.signal;
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log('Button clicked!');
}, { signal });
// To cancel the event listener:
controller.abort();
説明:
signal
をaddEventListener
メソッドのオプションとして渡します。controller.abort()
が呼び出されると、イベントリスナーは自動的に削除されます。
ReactコンポーネントでのAbortController
Reactでは、コンポーネントがアンマウントされたときにAbortController
を使用して非同期操作をキャンセルできます。これは、アンマウントされたコンポーネントの更新によって発生するメモリリークやエラーを防ぐために不可欠です。useEffect
フックを使用した例を次に示します。
import React, { useState, useEffect } from 'react';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data', { signal });
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
setData(data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Fetch error:', error);
}
}
}
fetchData();
return () => {
controller.abort(); // Cancel the fetch request when the component unmounts
};
}, []); // Empty dependency array ensures this effect runs only once on mount
return (
{data ? (
Data: {JSON.stringify(data)}
) : (
Loading...
)}
);
}
export default MyComponent;
説明:
useEffect
フック内にAbortController
を作成します。signal
をfetch
リクエストに渡します。useEffect
フックからクリーンアップ関数を返します。この関数は、コンポーネントがアンマウントされるときに呼び出されます。- クリーンアップ関数内で、
controller.abort()
を呼び出してfetchリクエストをキャンセルします。
高度なユースケース
AbortSignalのチェーン
場合によっては、複数のAbortSignal
をチェーンしたい場合があります。たとえば、子コンポーネントの操作をキャンセルする必要がある親コンポーネントがあるかもしれません。これは、新しいAbortController
を作成し、そのシグナルを親コンポーネントと子コンポーネントの両方に渡すことで実現できます。
サードパーティライブラリでのAbortControllerの使用
AbortSignal
を直接サポートしていないサードパーティライブラリを使用している場合は、ライブラリのキャンセルメカニズムと連携するようにコードを適合させる必要がある場合があります。これには、AbortSignal
を処理する独自の関数でライブラリの非同期関数をラップすることが含まれる場合があります。
AbortControllerを使用する利点
- パフォーマンスの向上: 不要な操作をキャンセルすると、ネットワークトラフィック、CPU使用率、およびメモリ消費を削減でき、特にリソースが制約されたデバイスでパフォーマンスが向上します。
- よりクリーンなコード:
AbortController
は、キャンセルの管理に対する標準化されたエレガントな方法を提供し、コードをより読みやすく、保守しやすくします。 - メモリリークの防止: アンマウントされたコンポーネントに関連付けられている非同期操作をキャンセルすると、メモリリークやアンマウントされたコンポーネントの更新によって発生するエラーを防ぎます。
- より良いユーザーエクスペリエンス: 無関係なリクエストをキャンセルすると、古い情報の表示を防ぎ、認識されるレイテンシを短縮することで、ユーザーエクスペリエンスを向上させることができます。
ブラウザの互換性
AbortController
は、Chrome、Firefox、Safari、Edgeなどの最新のブラウザで広くサポートされています。最新の情報については、MDN Web Docsの互換性テーブルを確認してください。
ポリフィル
AbortController
をネイティブにサポートしていない古いブラウザの場合は、ポリフィルを使用できます。ポリフィルとは、古いブラウザで新しい機能の機能を提供するコードのことです。オンラインで利用できるAbortController
ポリフィルがいくつかあります。
結論
AbortController
インターフェースは、JavaScriptで非同期操作を管理するための強力なツールです。AbortController
を使用すると、キャンセルを適切に処理する、よりクリーンで、より高性能で、より堅牢なコードを作成できます。APIからのデータの取得、タイマーの設定、イベントリスナーの管理など、AbortController
はWebアプリケーション全体の品質を向上させるのに役立ちます。