1720760516
2024-07-09 11:01:11
JavaScriptでは、すでにご存知かもしれませんが リクエストをキャンセルする方法: 使用できます xhr.abort() XHRと signal フェッチ用です。しかし、通常の Promise をキャンセルするにはどうすればよいでしょうか?
現在、JavaScript の Promise は、通常の Promise をキャンセルするための API をネイティブに提供していません。 そこで、次に Promise の結果を破棄/無視する方法について説明します。
方法 1: 新しい Promise.withResolvers() を使用する
現在使用できる新しいAPIは Promise.withResolvers()新しい Promise オブジェクトとそれを解決または拒否する 2 つの関数を含むオブジェクトを返します。
コードは次のようになります。
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
const { promise, resolve, reject } = Promise.withResolvers();
これを利用して、 cancel 方法:
const buildCancelableTask = T>(asyncFn: () => PromiseT>) => {
let rejected = false;
const { promise, resolve, reject } = Promise.withResolversT>();
return {
run: () => {
if (!rejected) {
asyncFn().then(resolve, reject);
}
return promise;
},
cancel: () => {
rejected = true;
reject(new Error('CanceledError'));
},
};
};
次に、次のテスト コードでこれを使用できます。
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
const ret = buildCancelableTask(async () => {
await sleep(1000);
return 'Hello';
});
(async () => {
try {
const val = await ret.run();
console.log('val: ', val);
} catch (err) {
console.log('err: ', err);
}
})();
setTimeout(() => {
ret.cancel();
}, 500);
ここでは、タスクが少なくとも 1000 ミリ秒かかるように事前設定していますが、次の 500 ミリ秒以内にキャンセルされるため、次のようになります。
これは本当のキャンセルではなく、早期の拒否であることに注意してください。元の asyncFn() 解決または拒否されるまで実行され続けますが、作成された約束は Promise.withResolvers すでに拒否されています。
方法 2: AbortController を使用する
フェッチ要求をキャンセルするのと同じように、早期拒否を実現するリスナーを実装できます。次のようになります。
const buildCancelableTask = T>(asyncFn: () => PromiseT>) => {
const abortController = new AbortController();
return {
run: () =>
new PromiseT>((resolve, reject) => {
const cancelTask = () => reject(new Error('CanceledError'));
if (abortController.signal.aborted) {
cancelTask();
return;
}
asyncFn().then(resolve, reject);
abortController.signal.addEventListener('abort', cancelTask);
}),
cancel: () => {
abortController.abort();
},
};
};
これは上記と同じ効果がありますが、AbortControllerを使用します。ここでは他のリスナーを使用することもできますが、AbortControllerを使用すると、 cancel 複数回使用しても、 'abort' イベントを複数回開催します。
このコードに基づいて、キャンセル可能なフェッチをさらに構築することができます。これは、以前のリクエスト結果を破棄して最新のリクエスト結果を使用する必要がある、連続リクエストなどのシナリオで役立ちます。
const buildCancelableFetch = T>(
requestFn: (signal: AbortSignal) => PromiseT>,
) => {
const abortController = new AbortController();
return {
run: () =>
new PromiseT>((resolve, reject) => {
if (abortController.signal.aborted) {
reject(new Error('CanceledError'));
return;
}
requestFn(abortController.signal).then(resolve, reject);
}),
cancel: () => {
abortController.abort();
},
};
};
const ret = buildCancelableFetch(async signal => {
return fetch('http://localhost:5000', { signal }).then(res =>
res.text(),
);
});
(async () => {
try {
const val = await ret.run();
console.log('val: ', val);
} catch (err) {
console.log('err: ', err);
}
})();
setTimeout(() => {
ret.cancel();
}, 500);
これはサーバー側の処理ロジックには影響しないことに注意してください。ブラウザがリクエストを破棄/キャンセルするだけです。つまり、ユーザー情報を更新するために POST リクエストを送信した場合、それが有効になる可能性があります。したがって、これは、新しいデータを取得するために GET リクエストが実行されるシナリオでより一般的に使用されます。
シンプルなシーケンシャルリクエスト React フックの構築
単純なシーケンシャルリクエスト React フックをさらにカプセル化できます。
import { useCallback, useRef } from 'react';
const buildCancelableFetch = T>(
requestFn: (signal: AbortSignal) => PromiseT>,
) => {
const abortController = new AbortController();
return {
run: () =>
new PromiseT>((resolve, reject) => {
if (abortController.signal.aborted) {
reject(new Error('CanceledError'));
return;
}
requestFn(abortController.signal).then(resolve, reject);
}),
cancel: () => {
abortController.abort();
},
};
};
function useLatestT>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
export function useSequentialRequestT>(
requestFn: (signal: AbortSignal) => PromiseT>,
) {
const requestFnRef = useLatest(requestFn);
const currentRequest = useRef{ cancel: () => void } | null>(null);
return useCallback(async () => {
if (currentRequest.current) {
currentRequest.current.cancel();
}
const { run, cancel } = buildCancelableFetch(requestFnRef.current);
currentRequest.current = { cancel };
return run().finally(() => {
if (currentRequest.current?.cancel === cancel) {
currentRequest.current = null;
}
});
}, [requestFnRef]);
}
次に、それを単純に使用します。
import { useSequentialRequest } from './useSequentialRequest';
export function App() {
const run = useSequentialRequest((signal: AbortSignal) =>
fetch('http://localhost:5000', { signal }).then((res) => res.text()),
);
return button onClick={run}>Run/button>;
}
この方法では、ボタンを複数回すばやくクリックすると、最新のリクエスト データのみが取得され、以前のリクエストは破棄されます。

最適化されたシーケンシャルリクエスト React フックの構築
より包括的なシーケンシャル リクエスト React フックが必要な場合は、上記の例にはまだ改善の余地があります。たとえば、次のようになります。
-
ユニークな
AbortController実際に必要になるまで保存しておくことで、毎回作成するコストを削減できます。 -
ジェネリックを使用して、任意の引数を渡すことができるリクエスト メソッドを構築できます。
import { useCallback, useRef } from 'react';
function useLatestT>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
export function useSequentialRequestArgs extends unknown[], Data>(
requestFn: (signal: AbortSignal, ...args: Args) => PromiseData>,
) {
const requestFnRef = useLatest(requestFn);
const running = useRef(false);
const abortController = useRefAbortController | null>(null);
return useCallback(
async (...args: Args) => {
if (running.current) {
abortController.current?.abort();
abortController.current = null;
}
running.current = true;
const controller = abortController.current ?? new AbortController();
abortController.current = controller;
return requestFnRef.current(controller.signal, ...args).finally(() => {
if (controller === abortController.current) {
running.current = false;
}
});
},
[requestFnRef],
);
}
注目すべきは、 finally ブロックでは、現在の controller 等しい abortController.current 競合状態を防ぐため、現在アクティブなリクエストが完了したときにのみ状態を更新するようにします。逆に、それらが等しくない場合は、 finally ブロックはキャンセルされたリクエストに属しており、 running.current 州。
import { useState } from 'react';
import { useSequentialRequest } from './useSequentialRequest';
export default function Home() {
const [data, setData] = useState('');
const run = useSequentialRequest(async (signal: AbortSignal, query: string) =>
fetch(`/api/hello?query=${query}`, { signal }).then((res) => res.text()),
);
const handleInput = async (queryStr: string) => {
try {
const res = await run(queryStr);
setData(res);
} catch {
// ignore
}
};
return (
input
placeholder="Please input"
onChange={(e) => {
handleInput(e.target.value);
}}
/>
div>Response Data: {data}/div>
/>
);
}
あなたはできる オンラインで体験する: 素早く入力してみてください。最新の応答を常に保持しながら、以前のリクエストをキャンセルします。

私のコンテンツが役に立ったと思われたら、 購読を検討する. 私は 毎週日曜日の週刊ニュースレター 最新の Web 開発アップデートをお届けします。ご協力ありがとうございます。
#JavaScript #で #Promise #を無効にする方法