약속 - 약속을 강제로 취소할 수 있습니까?
저는 ES6 Promise를 사용하여 모든 네트워크 데이터 검색을 관리하는데, 이를 강제로 취소해야 하는 경우가 있습니다.
기본적으로 시나리오는 부분적인 입력을 기반으로 검색을 수행해야 하는 백엔드로 요청이 위임된 UI에 대한 자동 검색을 수행하는 것입니다.이 네트워크 요청(#1)은 약간의 시간이 걸릴 수 있지만, 사용자는 계속해서 입력하고, 결국 다른 백엔드 호출(#2)을 트리거합니다.
여기 2번이 1번보다 당연히 우선하기 때문에 약속 포장 요청 1번을 취소하고 싶습니다.저는 이미 데이터 계층에 모든 약속의 캐시를 가지고 있기 때문에 이론적으로 #2에 대한 약속을 제출하려고 할 때 그것을 검색할 수 있습니다.
그러나 캐시에서 약속 #1을 검색하면 취소하려면 어떻게 해야 합니까?
누가 접근 방법을 제안해 줄 수 있습니까?
현대 자바스크립트에서 - 아니요
약속이 해결되었습니다(하하). 약속을 취소할 수는 없을 것 같습니다.
WHATWG(HTML 브라우저) 의(Node, Browser ) )의 등).AbortController
. 이를 사용하여 약속 자체보다 약속을 반환하는 기능을 취소할 수 있습니다.
// Take a signal parameter in the function that needs cancellation
async function somethingIWantToCancel({ signal } = {}) {
// either pass it directly to APIs that support it
// (fetch and most Node APIs do)
const response = await fetch('.../', { signal });
// return response.json;
// or if the API does not already support it -
// manually adapt your code to support signals:
const onAbort = (e) => {
// run any code relating to aborting here
};
signal.addEventListener('abort', onAbort, { once: true });
// and be sure to clean it up when the action you are performing
// is finished to avoid a leak
// ... sometime later ...
signal.removeEventListener('abort', onAbort);
}
// Usage
const ac = new AbortController();
setTimeout(() => ac.abort(), 1000); // give it a 1s timeout
try {
await somethingIWantToCancel({ signal: ac.signal });
} catch (e) {
if (e.name === 'AbortError') {
// deal with cancellation in caller, or ignore
} else {
throw e; // don't swallow errors :)
}
}
아니요. 아직 그럴 순 없어요.
ES6 약속은 아직 취소를 지원하지 않습니다.현재 진행 중이고, 디자인은 많은 분들이 정말 열심히 준비한 것입니다.사운드 캔슬레이션 시맨틱스는 정확하게 이해하기 어려우며 이 작업은 진행 중입니다."페치" 레포에 대한 흥미로운 토론, 한 토론 그리고 GH에 대한 여러 다른 레포에 대한 흥미로운 토론이 있지만, 제가 당신이라면 인내심을 가질 것입니다.
하지만, 하지만..취소는 정말 중요합니다!
그 문제의 현실은 취소가 클라이언트 측 프로그래밍에서 정말 중요한 시나리오라는 것입니다.웹 요청을 중단하는 것처럼 설명하는 경우는 중요하고 어디에나 있습니다.
그래서... 그 언어가 날 망쳤어요!
, 은 더 에 먼저 다 같은 . 그래서 그들은 다음과 같은 유용한 것들 없이 들어갔습니다..finally
그리고..cancel
- DOM을 통해 스펙으로 가고 있습니다.취소는 시간적 제약과 API 설계에 대한 보다 반복적인 접근 방식일 뿐이라는 후일의 생각이 아닙니다.
그래서 어떻게 해야 할까요?
다음과 같은 몇 가지 대안이 있습니다.
- 사양보다 훨씬 더 빠르게 이동할 수 있는 블루버드와 같은 타사 라이브러리를 사용하면 취소는 물론 기타 여러 가지 기능도 제공됩니다. 이것이 WhatsApp과 같은 대기업이 하는 일입니다.
- 취소 토큰을 전달합니다.
서드파티 라이브러리를 사용하는 것은 매우 명백합니다.토큰의 경우 메소드가 함수를 사용하도록 한 다음 다음과 같이 호출할 수 있습니다.
function getWithCancel(url, token) { // the token is for cancellation
var xhr = new XMLHttpRequest;
xhr.open("GET", url);
return new Promise(function(resolve, reject) {
xhr.onload = function() { resolve(xhr.responseText); });
token.cancel = function() { // SPECIFY CANCELLATION
xhr.abort(); // abort request
reject(new Error("Cancelled")); // reject the promise
};
xhr.onerror = reject;
});
};
그럼 다음을 수행할 수 있습니다.
var token = {};
var promise = getWithCancel("/someUrl", token);
// later we want to abort the promise:
token.cancel();
- -last
토큰 접근 방식은 크게 어렵지 않습니다.
function last(fn) {
var lastToken = { cancel: function(){} }; // start with no op
return function() {
lastToken.cancel();
var args = Array.prototype.slice.call(arguments);
args.push(lastToken);
return fn.apply(this, args);
};
}
그럼 다음을 수행할 수 있습니다.
var synced = last(getWithCancel);
synced("/url1?q=a"); // this will get canceled
synced("/url1?q=ab"); // this will get canceled too
synced("/url1?q=abc"); // this will get canceled too
synced("/url1?q=abcd").then(function() {
// only this will run
});
또한 Bacon이나 Rx 같은 라이브러리는 관찰 가능한 라이브러리이기 때문에 여기서 "빛을 발하는" 것이 아닙니다. 단지 라이브러리가 사양에 구애받지 않음으로써 사용자 수준의 약속이 갖는 이점과 같을 뿐입니다.ES2016에서 관측 가능한 것들이 토종이 되는 것을 두고 볼 수 있을 것 같습니다.하지만 그것들은 미리 타자를 치기에 적합합니다.
Abort Controller 사용
중단 컨트롤러를 사용하여 요청에 대한 약속이나 해결을 거부할 수 있습니다.
let controller = new AbortController();
let task = new Promise((resolve, reject) => {
// some logic ...
const abortListener = ({target}) => {
controller.signal.removeEventListener('abort', abortListener);
reject(target.reason);
}
controller.signal.addEventListener('abort', abortListener);
});
controller.abort('cancelled reason'); // task is now in rejected state
또한 메모리 유출을 방지하기 위해 중단 시 이벤트 수신기를 제거하는 것이 좋습니다.
에 에 에를 하면 중단으로 할 수 .controller.signal.aborted
부울 속성:
const res = task.catch((err) => (
controller.signal.aborted
? { value: err }
: { value: 'fallback' }
));
은 에 pending
영원한 신분하지만 그런 경우에는 당신도 얻을 수 없을 것입니다..catch
오류가 있으면 발사할 수 있습니다.
controller.abort();
new Promise((resolve, reject) => {
if(controller.signal.aborted) return;
}
가져오기를 취소하는 경우에도 동일한 작업이 수행됩니다.
let controller = new AbortController();
fetch(url, {
signal: controller.signal
});
컨트롤러만 통과하면 됩니다.
let controller = new AbortController();
fetch(url, controller);
하여 이 합니다를 를 취소합니다.controller.abort();
취소 가능한 약속에 대한 표준 제안이 실패했습니다.
약속은 약속을 이행하는 비동기 행동의 통제면이 아니며, 소유자와 소비자를 혼란스럽게 합니다.대신 전달된 토큰을 통해 취소할 수 있는 비동기 함수를 만듭니다.
또 로 쉽게 를 실행할 수 있게 만듭니다.Promise.race
:
예: 사용Promise.race
이전 체인의 효과를 취소하려면:
let cancel = () => {};
input.oninput = function(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
cancel();
let p = new Promise(resolve => cancel = resolve);
Promise.race([p, getSearchResults(term)]).then(results => {
if (results) {
console.log(`results for "${term}"`,results);
}
});
}
function getSearchResults(term) {
return new Promise(resolve => {
let timeout = 100 + Math.floor(Math.random() * 1900);
setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
});
}
Search: <input id="input">
을 ""를.undefined
다로 할 수 ."CancelledError"
대신.
이 실제로 , 입니다.fetch
.한다면fetch
취소 약속을 인수로 받아들이면 네트워크 활동을 취소할 수 있습니다.
저는 이 토론에서 "약속 취소 패턴"을 제안했는데, 정확히 다음을 제안하기 위해서입니다.fetch
이렇게 해요.
Mozilla JS 레퍼런스를 확인해보니 다음과 같습니다.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
확인해 보겠습니다.
var p1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 500, "one");
});
var p2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, "two");
});
Promise.race([p1, p2]).then(function(value) {
console.log(value); // "two"
// Both resolve, but p2 is faster
});
를에 p1, p2를 .Promise.race(...)
인수로서, 이것은 실제로 당신이 요구하는 새로운 해결 약속을 만드는 것입니다.
Node.js와 Electron의 경우, 자바스크립트(Prex)용 Promise Extensions를 사용하는 것을 적극 추천합니다.저자인 Ron Buckton은 TypeScript의 핵심 엔지니어 중 한 명이며 현재 TC39의 ECMAscript Cancelation 제안을 주도한 인물이기도 합니다.라이브러리는 잘 기록되어 있으며 일부 Prex가 표준에 도달할 가능성이 있습니다.
개인적인 의견과 C# 배경에서, 저는 Prex가 관리 스레드의 기존 취소 프레임워크를 모델로 한다는 점, 즉 다음과 같은 접근 방식을 기반으로 한다는 점이 매우 마음에 듭니다.CancellationTokenSource
/CancellationToken
NET API.제 경험으로는 관리되는 앱에서 강력한 취소 로직을 구현하는 데 매우 편리했습니다.
또한 브라우징파이를 이용하여 Prex를 번들로 제공하여 브라우저 내에서 동작하는지 확인하였습니다.
다음은 취소(Gist and RunKit, Prex를 사용한 경우)와 함께 지연되는 예입니다.CancellationToken
그리고.Deferred
):
// by @noseratio
// https://gist.github.com/noseratio/141a2df292b108ec4c147db4530379d2
// https://runkit.com/noseratio/cancellablepromise
const prex = require('prex');
/**
* A cancellable promise.
* @extends Promise
*/
class CancellablePromise extends Promise {
static get [Symbol.species]() {
// tinyurl.com/promise-constructor
return Promise;
}
constructor(executor, token) {
const withCancellation = async () => {
// create a new linked token source
const linkedSource = new prex.CancellationTokenSource(token? [token]: []);
try {
const linkedToken = linkedSource.token;
const deferred = new prex.Deferred();
linkedToken.register(() => deferred.reject(new prex.CancelError()));
executor({
resolve: value => deferred.resolve(value),
reject: error => deferred.reject(error),
token: linkedToken
});
await deferred.promise;
}
finally {
// this will also free all linkedToken registrations,
// so the executor doesn't have to worry about it
linkedSource.close();
}
};
super((resolve, reject) => withCancellation().then(resolve, reject));
}
}
/**
* A cancellable delay.
* @extends Promise
*/
class Delay extends CancellablePromise {
static get [Symbol.species]() { return Promise; }
constructor(delayMs, token) {
super(r => {
const id = setTimeout(r.resolve, delayMs);
r.token.register(() => clearTimeout(id));
}, token);
}
}
// main
async function main() {
const tokenSource = new prex.CancellationTokenSource();
const token = tokenSource.token;
setTimeout(() => tokenSource.cancel(), 2000); // cancel after 2000ms
let delay = 1000;
console.log(`delaying by ${delay}ms`);
await new Delay(delay, token);
console.log("successfully delayed."); // we should reach here
delay = 2000;
console.log(`delaying by ${delay}ms`);
await new Delay(delay, token);
console.log("successfully delayed."); // we should not reach here
}
main().catch(error => console.error(`Error caught, ${error}`));
취소는 경쟁이라는 점에 유의하십시오.되었을 수 ,만,까지와 함께)await
아니면then
했을 수 .), .당신이 이 경주를 어떻게 처리하느냐는 당신에게 달려 있지만, 전화를 해도 나쁘지 않습니다.token.throwIfCancellationRequested()
위에서 하는 것처럼 추가 시간이 필요해요.
저도 최근에 비슷한 문제에 직면했습니다.
저는 약속 기반의 클라이언트(네트워크가 아닌)가 있었고 UI를 원활하게 유지하기 위해 항상 최신 요청 데이터를 사용자에게 제공하고 싶었습니다.
에,Promise.race(...)
그리고.Promise.all(..)
나는 방금 내 마지막 요청 ID를 기억하기 시작했고 약속이 이행되었을 때 마지막 요청 ID와 일치할 때만 데이터를 렌더링하고 있었습니다.
누군가에게 도움이 되길 바랍니다.
https://www.npmjs.com/package/promise-abortable 참조
$ npm install promise-abortable
다음 작업을 마치기 전에 약속을 거부할 수 있습니다.
// Our function to cancel promises receives a promise and return the same one and a cancel function
const cancellablePromise = (promiseToCancel) => {
let cancel
const promise = new Promise((resolve, reject) => {
cancel = reject
promiseToCancel
.then(resolve)
.catch(reject)
})
return {promise, cancel}
}
// A simple promise to exeute a function with a delay
const waitAndExecute = (time, functionToExecute) => new Promise((resolve, reject) => {
timeInMs = time * 1000
setTimeout(()=>{
console.log(`Waited ${time} secs`)
resolve(functionToExecute())
}, timeInMs)
})
// The promise that we will cancel
const fetchURL = () => fetch('https://pokeapi.co/api/v2/pokemon/ditto/')
// Create a function that resolve in 1 seconds. (We will cancel it in 0.5 secs)
const {promise, cancel} = cancellablePromise(waitAndExecute(1, fetchURL))
promise
.then((res) => {
console.log('then', res) // This will executed in 1 second
})
.catch(() => {
console.log('catch') // We will force the promise reject in 0.5 seconds
})
waitAndExecute(0.5, cancel) // Cancel previous promise in 0.5 seconds, so it will be rejected before finishing. Commenting this line will make the promise resolve
유감스럽게도 호출이 이미 완료되었으므로 네트워크 탭에서 호출 해결을 볼 수 있습니다.당신의 코드는 그냥 무시할 겁니다.
외부 패키지에서 제공하는 Promise 하위 클래스를 사용하여 다음과 같이 수행할 수 있습니다. 라이브 데모
import CPromise from "c-promise2";
function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) {
return new CPromise((resolve, reject, {signal}) => {
fetch(url, {...fetchOptions, signal}).then(resolve, reject)
}, timeout)
}
const chain= fetchWithTimeout('http://localhost/')
.then(response => response.json())
.then(console.log, console.warn);
//chain.cancel(); call this to abort the promise and releated request
으로.AbortController
저는 며칠 동안 이에 대해 조사해 왔지만 아직도 중단된 이벤트 핸들러 내부의 약속을 거부하는 것은 접근 방식의 일부에 불과하다고 생각합니다.
아시다시피, 약속을 거부하는 것만으로 코드가 실행되기를 기다리지만, 약속을 거부하거나 해결한 후 실행 범위를 벗어나 실행되는 코드가 있으면 실행됩니다.이벤트 청취자 또는 비동기 통화 안에서는 계속 실행되어 사이클을 낭비하고 더 이상 필요하지 않은 것에 대한 메모리를 사용할 수도 있습니다.
어프로치부족
아래의 토막글을 실행할 때 2초 후 콘솔에 약속 거부 실행에서 파생된 출력과 보류 중인 작업에서 파생된 출력이 포함됩니다.그 약속은 거절될 것이고 그것을 기다리는 일은 계속될 수 있을 것이지만, 그 일은 그렇지 않을 것이고, 그것이 제 생각에는 이 연습의 요점입니다.
let abortController = new AbortController();
new Promise( ( resolve, reject ) => {
if ( abortController.signal.aborted ) return;
let abortHandler = () => {
reject( 'Aborted' );
};
abortController.signal.addEventListener( 'abort', abortHandler );
setTimeout( () => {
console.log( 'Work' );
console.log( 'More work' );
resolve( 'Work result' );
abortController.signal.removeEventListener( 'abort', abortHandler );
}, 2000 );
} )
.then( result => console.log( 'then:', result ) )
.catch( reason => console.error( 'catch:', reason ) );
setTimeout( () => abortController.abort(), 1000 );
그래서 저는 중단 이벤트 핸들러를 정의한 후에 다음에 대한 호출이 있어야 한다고 생각하게 됩니다.
if ( abortController.signal.aborted ) return;
작업이 수행되지 않도록 작업을 수행 중인 코드의 무의미한 점들(위의 if 블록에서 반환 전에 문장 추가).
제안
이 접근 방식은 몇 년 전의 취소 가능한 토큰 제안을 약간 상기시켜 주지만 실제로는 작업이 헛되이 수행되는 것을 막을 수 있습니다.콘솔 출력은 이제 중단 오류일 뿐이며 작업이 진행 중이고 중간에 취소되면 루프 본체의 시작과 같이 이전에 설명한 것처럼 처리의 합리적인 단계에서 중지할 수 있습니다.
let abortController = new AbortController();
new Promise( ( resolve, reject ) => {
if ( abortController.signal.aborted ) return;
let abortHandler = () => {
reject( 'Aborted' );
};
abortController.signal.addEventListener( 'abort', abortHandler );
setTimeout( () => {
if ( abortController.signal.aborted ) return;
console.log( 'Work' );
if ( abortController.signal.aborted ) return;
console.log( 'More work' );
resolve( 'Work result' );
abortController.signal.removeEventListener( 'abort', abortHandler );
}, 2000 );
} )
.then( result => console.log( 'then:', result ) )
.catch( reason => console.error( 'catch:', reason ) );
setTimeout( () => abortController.abort(), 1000 );
저는 여기에 게시된 솔루션들이 조금 읽기 어렵다고 판단하여 제 생각에 사용하기 쉬운 도우미 기능을 만들었습니다.
도우미 기능을 사용하면 현재 통화가 이미 사용되지 않았는지 여부에 대한 정보에 액세스할 수 있습니다.이 정보를 사용하면 기능 자체가 그에 따라 처리해야 합니다(일반적으로 단순히 반환만 함).
// Typescript
export function obsoletableFn<Res, Args extends unknown[]>(
fn: (isObsolete: () => boolean, ...args: Args) => Promise<Res>,
): (...args: Args) => Promise<Res> {
let lastCaller = null;
return (...args: Args) => {
const me = Symbol();
lastCaller = me;
const isObsolete = () => lastCaller !== me;
return fn(isObsolete, ...args);
};
}
// helper function
function obsoletableFn(fn) {
let lastCaller = null;
return (...args) => {
const me = Symbol();
lastCaller = me;
const isObsolete = () => lastCaller !== me;
return fn(isObsolete, ...args);
};
}
const simulateRequest = () => new Promise(resolve => setTimeout(resolve, Math.random() * 2000 + 1000));
// usage
const myFireAndForgetFn = obsoletableFn(async(isObsolete, x) => {
console.log(x, 'starting');
await simulateRequest();
if (isObsolete()) {
console.log(x, 'is obsolete');
// return, as there is already a more recent call running
return;
}
console.log(x, 'is not obsolete');
document.querySelector('div').innerHTML = `Response ${x}`;
});
myFireAndForgetFn('A');
myFireAndForgetFn('B');
<div>Waiting for response...</div>
그래서 저는 사용자 입력 시 취소해야 했던 비동기 기능이 있지만 마우스 제어를 수반하는 장시간 실행 기능입니다.
저는 p-queue를 사용하고 제 기능에 있는 각 라인을 추가했고 취소 신호를 입력하는 관측치를 가지고 있습니다.대기열이 처리를 시작하는 모든 것은 어떤 일이 있어도 실행되지만, 그 후에는 대기열을 지움으로써 모든 것을 취소할 수 있습니다.대기열에 추가하는 작업이 짧을수록 취소 신호를 받은 후 더 빨리 종료할 수 있습니다.예제에서 사용하는 라이너 대신 전체 코드 덩어리를 큐에 던질 수 있습니다.
p-queue 릴리스 버전 6은 commonjs, 7+ 스위치에서 ESM으로 작동하며 앱이 깨질 수 있습니다.전자/유형 스크립트/웹팩을 깨트립니다.
const cancellable_function = async () => {
const queue = new PQueue({concurrency:1});
queue.pause();
queue.addAll([
async () => await move_mouse({...}),
async () => await mouse_click({...}),
])
for await (const item of items) {
queue.addAll([
async () => await do_something({...}),
async () => await do_something_else({...}),
])
}
const {information} = await get_information();
queue.addAll([
async () => await move_mouse({...}),
async () => await mouse_click({...}),
])
cancel_signal$.pipe(take(1)).subscribe(() => {
queue.clear();
});
queue.start();
await queue.onEmpty()
}
왜냐하면 @jib이 제 수정을 거부해서 여기에 제 답변을 올립니다.그냥 @jib의 답변을 약간의 코멘트로 수정하고 좀 더 이해하기 쉬운 변수 이름을 사용하는 것입니다.
아래에서는 두 가지 다른 방법의 예를 보여드리겠습니다. 하나는 해결()이고 다른 하나는 거부()입니다.
let cancelCallback = () => {};
input.oninput = function(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
cancelCallback(); //cancel previous promise by calling cancelCallback()
let setCancelCallbackPromise = () => {
return new Promise((resolve, reject) => {
// set cancelCallback when running this promise
cancelCallback = () => {
// pass cancel messages by resolve()
return resolve('Canceled');
};
})
}
Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
// check if the calling of resolve() is from cancelCallback() or getSearchResults()
if (results == 'Canceled') {
console.log("error(by resolve): ", results);
} else {
console.log(`results for "${term}"`, results);
}
});
}
input2.oninput = function(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
cancelCallback(); //cancel previous promise by calling cancelCallback()
let setCancelCallbackPromise = () => {
return new Promise((resolve, reject) => {
// set cancelCallback when running this promise
cancelCallback = () => {
// pass cancel messages by reject()
return reject('Canceled');
};
})
}
Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
// check if the calling of resolve() is from cancelCallback() or getSearchResults()
if (results !== 'Canceled') {
console.log(`results for "${term}"`, results);
}
}).catch(error => {
console.log("error(by reject): ", error);
})
}
function getSearchResults(term) {
return new Promise(resolve => {
let timeout = 100 + Math.floor(Math.random() * 1900);
setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
});
}
Search(use resolve): <input id="input">
<br> Search2(use reject and catch error): <input id="input2">
언급URL : https://stackoverflow.com/questions/30233302/promise-is-it-possible-to-force-cancel-a-promise
'itsource' 카테고리의 다른 글
다른 파워셸 스크립트에서 변수 로드 (0) | 2023.09.24 |
---|---|
에 상당하는 주석 (0) | 2023.09.24 |
C 네임스페이스 이해 (0) | 2023.09.24 |
MySQL 테이블에 값을 삽입할 때 자동 증분 기본 키의 속성을 재정의하는 방법은 무엇입니까? (0) | 2023.09.24 |
XML 인스턴스 문서에서 XSD 스키마를 생성하는 도구가 있습니까? (0) | 2023.09.24 |