Обіцянка - чи можна змусити скасувати обіцянку


91

Я використовую ES6 Promises для керування всіма своїми пошуками мережевих даних, і в деяких ситуаціях мені потрібно змусити їх скасувати.

В основному сценарій такий, що я маю пошук наперед за користувальницьким інтерфейсом, де запит делегується до серверної бази, щоб здійснити пошук на основі часткового вводу. Хоча цей мережевий запит (# 1) може зайняти трохи часу, користувач продовжує вводити текст, що в кінцевому підсумку ініціює черговий внутрішній виклик (# 2)

Тут №2, природно, має перевагу над №1, тому я хотів би скасувати запит на обгортання Promise №1. У мене вже є кеш усіх Promises на рівні даних, тому я можу теоретично отримати його, оскільки намагаюся подати Promise для №2.

Але як я можу скасувати Promise # 1, коли я отримую його з кешу?

Хтось може запропонувати підхід?


2
це варіант використовувати якийсь еквівалент функції розриву, щоб не запускати часто і ставати запитами застарілості? Скажімо, затримка 300 мс зробить свою справу. Наприклад, у Lodash є одна з реалізацій - lodash.com/docs#debounce
shershen

Це коли речі, такі як Бекон та Rx, приносять користь.
elclanrs

@shershen так - ми маємо це, але це стосується не стільки питання користувальницького інтерфейсу ... запит сервера може зайняти трохи часу, тому я хочу мати можливість скасувати Обіцянки ...
Moonwalker


Спробуйте Observables від Rxjs
FieryCod

Відповіді:


164

Ні. Ми поки не можемо цього зробити.

ES6 обіцянку не підтримують скасування ще . Це вже в дорозі, і над його дизайном - це те, над чим багато людей працювали по-справжньому. Звукова семантика скасування важко отримати права , і це робота. Існують цікаві дебати щодо репо "вибору", про есдискусу та щодо кількох інших репо про GH, але на вашому місці я б просто набрався терпіння.

Але, але, але .. скасування дійсно важливо!

Це дійсність у тому, що скасування дійсно є важливим сценарієм програмування на стороні клієнта. Випадки, які ви описуєте як скасування веб-запитів, важливі, і вони є скрізь.

Отож ... мова мене накрутила!

Так, вибачте за це. Обіцянки повинні були увійти спочатку, перш ніж були визначені подальші речі, - тому вони увійшли без корисних речей, таких як .finallyі .cancel- проте це вже на шляху, до специфікації через DOM. Скасування не є додатковою думкою, це лише обмеження у часі та більш ітеративний підхід до проектування API.

То що я можу зробити?

У вас є кілька альтернатив:

  • Використовуйте сторонні бібліотеки, такі як bluebird, які можуть рухатися набагато швидше, ніж специфікація, і, таким чином, скасовують їх, а також купу інших смакот - це те, що роблять великі компанії, такі як 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, коли спостережувані стануть рідними. Вони є чепурних для машинописного , хоча.


25
Бенджамін, дуже сподобався прочитати вашу відповідь. Дуже продуманий, структурований, чіткий та з хорошими практичними прикладами та альтернативами. Дуже корисно. Дякую.
Moonwalker

@FranciscoPresencia токени скасування вже готові як пропозиція першого етапу.
Бенджамін Груенбаум,

Де ми можемо прочитати це скасування на основі маркера? Де пропозиція?
шкода

@harm пропозиція мертва на етапі 1.
Бенджамін Груенбаум,

1
Мені подобається робота Рона, але я думаю, нам слід трохи почекати, перш ніж робити рекомендації для бібліотек, якими люди ще не користуються:] Дякую за посилання, хоча я перевірю це!
Бенджамін

24

Стандартні пропозиції щодо скасованих обіцянок провалились.

Обіцянка не є поверхнею керування асинхронною дією, що її виконує; плутає власника зі споживачем. Натомість створіть асинхронні функції, які можна скасувати за допомогою переданого маркера.

Ще одна обіцянка робить чудовий маркер, що робить скасування легким для реалізації за допомогою 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взяти обіцянку скасування як аргумент, то це могло б скасувати мережеву активність.

Я запропонував цей "шаблон скасування обіцянки" на 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(...)якості аргументів, це насправді створює нову обіцянку вирішення, що саме вам потрібно.


ПРИГОЖНО - це, можливо, саме те, що мені потрібно. Я спробую.
Moonwalker

Якщо у вас з цим проблеми, ви можете вставити сюди код, щоб я міг вам допомогти :)
nikola-miljkovic

5
Спробував. Не зовсім там. Це вирішує найшвидший Проміс ... Мені потрібно завжди вирішувати останні подані, тобто беззастережно скасувати будь-які попередні Обіцянки ..
Moonwalker

1
Таким чином, усі інші обіцянки вже не обробляються, ви насправді не можете скасувати обіцянку.
nikola-miljkovic

Я спробував, друга обіцянка (одна в цій колишній) не дозволяє завершити процес :(
morteza ataiy

3

Для Node.js та Electron я настійно рекомендую використовувати Promise Extensions для JavaScript (Prex) . Її автор Рон Бактон є одним із ключових інженерів TypeScript, а також відповідальним за поточну пропозицію TC39 щодо скасування ECMAScript . Бібліотека добре задокументована, і деякі шанси Prex досягнуть рівня.

Особисто зауваживши, що я виходжу з фону C #, мені дуже подобається той факт, що Prex створений за зразком існуючого скасування в рамках керованих потоків , тобто на основі підходу, застосованого до CancellationTokenSource/ CancellationToken.NET API. З мого досвіду, вони були дуже зручними для впровадження надійної логіки скасування в керованих додатках.

Я також перевірив його роботу в браузері, об’єднавши Prex за допомогою Browserify .

Ось приклад затримки із скасуванням ( Gist та 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()додаткового часу, як це робиться вище.


1

Нещодавно я стикався з подібною проблемою.

У мене був клієнт на основі обіцянок (а не мережевий), і я хотів завжди передавати користувачеві останні запитувані дані, щоб користувальницький інтерфейс був гладким.

Після боротьби з ідеєю скасування, Promise.race(...)і Promise.all(..)я тільки почав згадувати свій останній ідентифікатор запиту, і коли обіцянка була виконана, я надавав свої дані лише тоді, коли вона відповідала ідентифікатору останнього запиту.

Сподіваюся, це комусь допомагає.


Сломське питання не в тому, що показувати в інтерфейсі.
Йдеться


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's anwser з деякими коментарями та використанням більш зрозумілих імен змінних.

Нижче я просто наводжу приклади двох різних методів: один - resol (), інший - 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">

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.