약속-약속을 강제로 취소 할 수 있습니까?


91

ES6 Promises를 사용하여 모든 네트워크 데이터 검색을 관리하고 강제 취소해야하는 상황이 있습니다.

기본적으로 시나리오는 요청이 백엔드에 위임되는 UI에서 자동 완성 검색을 수행하여 부분 입력을 기반으로 검색을 수행해야합니다. 이 네트워크 요청 (# 1)은 약간의 시간이 걸릴 수 있지만 사용자는 계속 입력하여 결국 다른 백엔드 호출 (# 2)을 트리거합니다.

여기서 # 2는 당연히 # 1보다 우선하므로 Promise 래핑 요청 # 1을 취소하고 싶습니다. 이미 데이터 영역에 모든 Promise 캐시가 있으므로 # 2에 대한 Promise를 제출하려고 할 때 이론적으로 검색 할 수 있습니다.

하지만 캐시에서 Promise # 1을 검색 한 후 어떻게 취소합니까?

누구든지 접근 방식을 제안 할 수 있습니까?


2
자주 트리거하지 않고 단호한 요청이되지 않도록 디 바운스 기능과 동등한 것을 사용하는 옵션입니까? 300ms 지연이 트릭을 수행한다고 가정하십시오. 예를 들어 Lodash는 구현 중 하나를 가지고 있습니다 -lodash.com/docs#debounce
shershen

이것은 Bacon 및 Rx와 같은 것이 유용 할 때입니다.
elclanrs

@shershen 예-우리는 이것을 가지고 있지만 이것은 UI 문제에 관한 것이 아닙니다. 서버 쿼리는 약간의 시간이 걸릴 수 있으므로 약속을 취소하고 싶습니다 ...
Moonwalker


Rxjs
FieryCod

답변:


163

아니요. 아직 할 수 없습니다.

ES6 약속은 아직 취소를 지원하지 않습니다 . 진행 중이며 디자인은 많은 사람들이 정말 열심히 노력한 것입니다. 사운드 취소 의미론은 제대로 이해하기 어렵고 진행중인 작업입니다. "가져 오기"저장소, esdiscuss 및 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와 같은 라이브러리는 관찰 가능한 라이브러리이기 때문에 여기에서 "빛나지"않습니다. 스펙에 얽매이지 않음으로써 사용자 수준의 약속 라이브러리와 동일한 이점이 있습니다. Observable이 네이티브가 될 때 ES2016에서 볼 수 있기를 기다릴 것입니다. 그들은 이다 그러나 선행 입력을위한 멋진.


25
Benjamin, 정말 당신의 답변을 읽는 것을 즐겼습니다. 매우 잘 생각되고, 구조화되고, 명료하며, 좋은 실용적인 예와 대안이 있습니다. 정말 도움이되었습니다. 감사합니다.
Moonwalker

@FranciscoPresencia 취소 토큰은 1 단계 제안으로 진행 중입니다.
Benjamin Gruenbaum

이 토큰 기반 취소에 대해 어디에서 읽을 수 있습니까? 제안은 어디에 있습니까?
해로움

@harm 제안은 1 단계에서 죽었습니다.
Benjamin Gruenbaum

1
나는 Ron의 작업을 좋아하지만, 사람들이 아직 사용하지 않는 도서관을 추천하기 전에 조금 기다려야한다고 생각합니다.
Benjamin Gruenbaum

24

취소 가능한 약속에 대한 표준 제안이 실패했습니다.

promise는이를 수행하는 비동기 작업의 제어 영역이 아닙니다. 소유자와 소비자를 혼동합니다. 대신 전달 된 토큰을 통해 취소 할 수있는 비동기 함수 를 만듭니다 .

또 다른 약속은 훌륭한 토큰을 만들어 취소를 쉽게 구현할 수 있도록합니다 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. 경우 fetchA는 인수로 약속을 취소 걸릴했다, 다음은 네트워크 활동을 취소 할 수 있습니다.

내가 한 제안 이의 "약속 패턴 취소"정확히 제안, ES를-토론 fetch이 작업을 수행.


@jib 왜 내 수정을 거부합니까? 나는 그것을 명확히한다.
allenyllee

8

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(...)를 인수로 넣었습니다 . 이것은 실제로 여러분이 필요로하는 새로운 결의 약속을 생성하는 것입니다.


NICE-이것은 내가 정확히 필요한 것일 수 있습니다. 나는 그것을 시도 할 것이다.
Moonwalker

문제가 있으면 여기에 코드를 붙여 넣어 도와 드릴 수 있습니다. :)
nikola-miljkovic

5
시도했습니다. 거기는 아닙니다. 이것은 가장 빠른 약속을 해결합니다 ... 항상 가장 최근에 제출 된 약속을 해결해야합니다. 즉, 이전 약속을 무조건 취소합니다.
Moonwalker

1
이렇게하면 다른 모든 약속이 더 이상 처리되지 않으므로 실제로 약속을 취소 할 수 없습니다.
nikola-miljkovic

나는 그것을 시도했다, 두 번째 약속 (이 예에서 하나) 프로세스를 종료하지 마십시오 :(
morteza ataiy

3

Node.js 및 Electron의 경우 Promise Extensions for JavaScript (Prex)를 사용하는 것이 좋습니다 . 저자 인 Ron Buckton 은 주요 TypeScript 엔지니어 중 한 명이며 현재 TC39의 ECMAScript 취소 제안 의 배후에있는 사람이기도합니다 . 라이브러리는 잘 문서화되어 있으며 Prex가 표준으로 만들 가능성이 있습니다.

개인적인 메모와 C # 배경에서 나는 Prex가 Managed Threads 프레임 워크 의 기존 취소 , 즉 CancellationTokenSource/ CancellationToken.NET API로 취한 접근 방식을 기반으로 모델링되었다는 사실을 매우 좋아 합니다. 내 경험상 관리되는 앱에서 강력한 취소 논리를 구현하는 데 매우 편리했습니다.

또한 Browserify를 사용하여 Prex를 번들링하여 브라우저 내에서 작동하는지 확인했습니다 .

다음은 취소로 인한 지연의 예입니다 ( GistRunKit , 해당 및에 Prex 사용 ).CancellationTokenDeferred

// 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(), 제가 ​​위에서 한 것처럼 여분의 시간 을 부르는 것은 아프지 않습니다 .


1

나는 최근 비슷한 문제에 직면했습니다.

약속 기반 클라이언트 (네트워크가 아님)가 있었고 UI를 원활하게 유지하기 위해 항상 사용자에게 최신 요청 데이터를 제공하고 싶었습니다.

취소 아이디어로 어려움을 겪고, 후 Promise.race(...)그리고 Promise.all(..)난 그냥 내 마지막 요청 ID를 기억하기 시작하고 약속이 성취되었을 때 그것이 마지막 요청의 ID를 일치 할 때 난 단지 내 데이터를 렌더링했다.

누군가에게 도움이되기를 바랍니다.


Slomski는 UI에 무엇을 표시할지에 대한 질문이 아닙니다. 약속 취소에 대해
CyberAbhay


0

완료하기 전에 약속을 거부 할 수 있습니다.

// 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

안타깝게도 가져 오기 호출이 이미 완료되었으므로 네트워크 탭에서 호출이 해결되는 것을 볼 수 있습니다. 귀하의 코드는 그것을 무시합니다.


0

외부 패키지에서 제공하는 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

-1

@jib가 내 수정을 거부하기 때문에 여기에 내 답변을 게시합니다. 주석이 달린 @jib 의 anwser 를 수정하고 더 이해하기 쉬운 변수 이름을 사용하는 것입니다.

아래에서는 두 가지 다른 방법의 예를 보여줍니다. 하나는 resolve ()이고 다른 하나는 reject ()입니다.

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">

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.