Как отменить последнее обещание, если оно не решено? - PullRequest
3 голосов
/ 19 сентября 2019

Допустим, у меня есть функция поиска для выполнения HTTP-вызова.Каждый звонок может занять разное количество времени.Поэтому мне нужно отменить последний HTTP-запрос и ждать только последнего вызова

async function search(timeout){

   const data = await promise(timeout)
   return data;

}
// the promise function is only for visualizing an http call
function promise(timeout){
   return new Promise(resolve,reject){
       setTimeout(function(){      
           resolve()
       },timeout) 
   }
}
search(200)
.then(function(){console.log('search1 resolved')})
.catch(function() {console.log('search1 rejected')})
search(2000)
.then(function(){console.log('search2 resolved')})
.catch(function(){console.log('search2 rejected')})
search(1000)
.then(function(){console.log('search3 resolved')})
.catch(function(){console.log('search3 rejected')})

Нужно увидеть «поиск1 решен» «поиск2 отклонен» «поиск3 разрешен»

Как мне добитьсяэтот сценарий?

Ответы [ 3 ]

2 голосов
/ 19 сентября 2019

Вы можете определить фабричную функцию для инкапсуляции вашего метода search() с запрошенным поведением отмены.Обратите внимание, что хотя конструкторы Promise обычно считаются антишаблоном , в этом случае необходимо сохранить ссылку на каждую функцию reject() в наборе pending, чтобы реализовать раннюю отмену.

function cancellable(fn) {
  const pending = new Set();

  return function() {
    return new Promise(async (resolve, reject) => {
      let settle;
      let result;

      try {
        pending.add(reject);
        settle = resolve;
        result = await Promise.resolve(fn.apply(this, arguments));
      } catch (error) {
        settle = reject;
        result = error;
      }

      // if this promise has not been cancelled
      if (pending.has(reject)) {
        // cancel the pending promises from calls made before this
        for (const cancel of pending) {
          pending.delete(cancel);

          if (cancel !== reject) {
            cancel();
          } else {
            break;
          }
        }

        settle(result);
      }
    });
  };
}

// internal API function
function searchImpl(timeout) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, timeout);
  });
}

// pass your internal API function to cancellable()
// and use the return value as the external API function
const search = cancellable(searchImpl);

search(200).then(() => {
  console.log('search1 resolved');
}, () => {
  console.log('search1 rejected');
});

search(2000).then(() => {
  console.log('search2 resolved');
}, () => {
  console.log('search2 rejected');
});

search(1000).then(() => {
  console.log('search3 resolved');
}, () => {
  console.log('search3 rejected');
});

search(500).then(function() {
  console.log('search4 resolved');
}, () => {
  console.log('search4 rejected');
});

Эта фабричная функция использует итерацию порядка вставки Set, чтобы отменить только ожидающие обещания, возвращаемые вызовами, выполненными до вызова, возвращающего обещаниеэто только что завершилось.


Обратите внимание, что отмена обещания с использованием reject() не завершает какой-либо базовый асинхронный процесс, инициированный созданием обещания.Каждый HTTP-запрос будет продолжен до завершения, как и любые другие внутренние обработчики, которые вызываются в search() до выполнения обещания.

Все, что делает cancellation(), вызывает переход внутреннего состояния возвращаемого обещания из в ожидании в отклонено вместо выполнено , если позднееОбещание устанавливается первым, так что соответствующий код будет вызываться соответствующим обработчиком для разрешения обещания.

1 голос
/ 19 сентября 2019

Аналогично ответу PatrickRoberts, я бы предложил использовать Map для ведения списка ожидающих обещаний.

Однако я бы не поддерживал ссылку на обратный вызов reject вне обещанияконструктор.Я бы предложил отказаться от идеи отклонить устаревшее обещание.Вместо этого просто игнорируйте это.Оберните его в обещание, которое никогда не разрешает и не отклоняет, а просто остается мертвым объектом обещания, которое никогда не изменяет состояние.Фактически, это безмолвное обещание может быть одинаковым для каждого случая, когда оно вам нужно.

Вот как это может выглядеть:

const delay = (timeout, data) => new Promise(resolve => setTimeout(() => resolve(data), timeout));
const godot = new Promise(() => null);

const search = (function () { // closure...
    const requests = new Map; // ... so to have shared variables
    let id = 1;
    
    return async function search() {
        let duration = Math.floor(Math.random() * 2000);
        let request = delay(duration, "data" + id); // This would be an HTTP request
        requests.set(request, id++);
        let data = await request;
        if (!requests.has(request)) return godot; // Never resolve...
        for (let [pendingRequest, pendingId] of requests) {
            if (pendingRequest === request) break;
            requests.delete(pendingRequest);
            // Just for demo we output something. 
            // Not needed in a real scenario:
            console.log("ignoring search " + pendingId);
        }
        requests.delete(request);
        return data;
    }    
})();

const reportSuccess = data => console.log("search resolved with " + data);
const reportError = err => console.log('search rejected with ' + err);

// Generate a search at regular intervals.
// In a real scenario this could happen in response to key events.
// Each promise resolves with a random duration.
setInterval(() => search().then(reportSuccess).catch(reportError), 100);
0 голосов
/ 19 сентября 2019

Обещания не могут быть отменены как таковые, но отменяются в ограниченном смысле, вызывая их отклонение.

Учитывая это, отмена может быть достигнута с небольшим количеством детализации около Promise.race() и функция возврата обещания, которую вы хотите отменить.

function makeCancellable(fn) {
    var reject_; // cache for the latest `reject` executable
    return function(...params) {
        if(reject_) reject_(new Error('_cancelled_')); // If previous reject_ exists, cancel it.
                                                       // Note, this has an effect only if the previous race is still pending.
        let canceller = new Promise((resolve, reject) => { // create canceller promise
            reject_ = reject; // cache the canceller's `reject` executable
        });
        return Promise.race([canceller, fn.apply(null, params)]); // now race the promise of interest against the canceller
    }
}

Если ваша функция вызова http названа httpRequest (promise сбивает с толку):

const search = makeCancellable(httpRequest);

Теперь каждый раз, когда вызывается search(), кешируемый исполняемый файл reject вызывается для «отмены» предыдущего поиска (если он существует и его раса еще не выполнена).

// Search 1: straightforward - nothing to cancel - httpRequest(200) is called
search(200)
.then(function() { console.log('search1 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

// Search 2: search 1 is cancelled and its catch callback fires - httpRequest(2000) is called
search(2000)
.then(function() { console.log('search2 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

// Search 3: search 2 is cancelled and its catch callback fires - httpRequest(1000) is called
search(1000)
.then(function() { console.log('search3 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

При необходимости обратные вызовы catch могут проверять err.message === '_cancelled_', чтобы различать отмену и другие причины отклонения.

...