Подтвердить что ты не робот

Обещание - можно ли отменить обещание

Я использую ES6 Promises для управления всеми моими сетевыми данными, и есть ситуации, когда мне нужно принудительно отменить их.

В основном сценарий таков, что у меня есть поиск по принципу "вперед" в пользовательском интерфейсе, где запрос делегирован на бэкэнд, чтобы выполнить поиск на основе частичного ввода. Хотя этот сетевой запрос (№ 1) может занять немного времени, пользователь продолжает вводить код, который в конечном итоге вызывает другой бэкэнд-вызов (# 2)

Здесь # 2, естественно, имеет приоритет над # 1, поэтому я хотел бы отменить запрос об упаковке Promise # 1. У меня уже есть кеш всех Promises в слое данных, поэтому я могу теоретически получить его, поскольку я пытаюсь отправить Promise для # 2.

Но как мне отменить Promise # 1, как только я извлечу его из кеша?

Может ли кто-нибудь предложить подход?

4b9b3361

Ответ 1

Нет. Мы еще не можем этого сделать.

ES6 promises пока не поддерживают отмену. Это на своем пути, и его дизайн - это то, над чем много работало. Семантика отмены звука трудно понять, и это незавершенное производство. Есть интересные дебаты о репозитории "выборки", о esdiscuss и о нескольких других репозиториях GH, но я был бы терпелив, если бы был вами.

Но, но, но.. отмена действительно важна!

Это, реальность дела, это отмена, действительно важный сценарий в программировании на стороне клиента. Случаи, которые вы описываете как прерывание веб-запросов, важны, и они повсюду.

Итак... язык напортачил меня!

Да, извините. promises должен был попасть первым, прежде чем было указано что-то еще, - поэтому они вошли без какого-либо полезного материала, такого как .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, когда наблюдаемые станут родными. Однако они носят отличный характер.

Ответ 2

Стандартные предложения для отменяемых обещаний потерпели неудачу.

Обещание не является контрольной поверхностью для выполняющего его асинхронного действия; путает владельца с потребителем. Вместо этого создайте асинхронные функции, которые можно отменить с помощью некоторого переданного токена.

Еще одно обещание делает хороший токен, облегчая реализацию отмены с помощью 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">

Ответ 3

Я проверил ссылку на 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(...), это фактически создает новое обещание разрешения, что вам и нужно.

Ответ 4

Для Node.js и Electron я настоятельно рекомендую использовать Расширения Promise для JavaScript (Prex). Его автор Рон Бактон является одним из ключевых инженеров TypeScript, а также парнем, стоящим за текущим предложением TCa ECMAScript Cancellation. Библиотека хорошо документирована, и есть вероятность, что некоторые из Prex будут соответствовать стандарту.

Что касается личной информации и я имею в виду обширный опыт работы с С#, мне очень нравится тот факт, что Prex смоделирован на основе существующей структуры Cancellation in Managed Threads, т.е. основан на подходе, принятом в CancellationTokenSource/[TG41.NET API. По моему опыту, они были очень удобны для реализации надежной логики отмены в управляемых приложениях.

Я также проверил его работу в браузере, связав Prex с помощью Browserify.

Вот пример задержки с отменой, используя prex.CancellationTokenSource:

// https://stackoverflow.com/a/53093799

const prex = require('prex');

// delayWithCancellation
function delayWithCancellation(timeoutMs, token) {
  console.log('delayWithCancellation: ${timeoutMs}');

  return createCancellablePromise((resolve, reject, setCancelListener) => {
    token.throwIfCancellationRequested();
    const id = setTimeout(resolve, timeoutMs);
    setCancelListener(e => clearTimeout(id));
  }, token);
}

// main
async function main() {
  const tokenSource = new prex.CancellationTokenSource();
  setTimeout(() => tokenSource.cancel(), 2000); // cancel after 1500ms

  const token = tokenSource.token;

  await delayWithCancellation(1000, token);
  console.log("successfully delayed."); // we should reach here

  await delayWithCancellation(1500, token);
  console.log("successfully delayed."); // we should not reach here
}

// createCancellablePromise - create a Promise that gets rejected
// when cancellation is requested on the token source
async function createCancellablePromise(executor, token) {
  if (!token) {
    return await new Promise(executor);
  }

  // prex.Deferred is similar to TaskCompletionsource in .NET
  const d = new prex.Deferred();

  // function executor(resolve, reject, setCancelListener)
  // function oncancel(CancelError)
  executor(d.resolve, d.reject, oncancel =>
    d.oncancel = oncancel);

  const reg = token.register(() => {
    // the token cancellation callback is synchronous,
    // and so is the d.oncancel callback
    try {
      // capture the CancelError
      token.throwIfCancellationRequested();
    }
    catch (e) {
      try {
        d.oncancel && d.oncancel(e);
        // reject here if d.oncancel did not resolve/reject
        d.reject(e);
      }
      catch (e2) {
        d.reject(e2);
      }
    }
  });

  try {
    await d.promise;
  }
  finally {
    reg.unregister();
  }
}

main().catch(e => console.log(e));

Обратите внимание, что отмена - это гонка. То есть, обещание могло быть успешно выполнено, но к тому времени, когда вы его соблюдаете (с await или then), аннулирование также может быть инициировано. Вам решать, как вы справляетесь с этой гонкой, но не больно называть token.throwIfCancellationRequested() дополнительным временем, как я делаю выше.

Ответ 5

Я недавно столкнулся с подобной проблемой.

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

После борьбы с идеей отмены, Promise.race(...) и Promise.all(..) я только начал запоминать свой последний идентификатор запроса, и когда обещание было выполнено, я рендерил свои данные только тогда, когда они соответствовали идентификатору последнего запроса,

Надеюсь, это поможет кому-то.

Ответ 7

Потому что @jib отклоняет мои изменения, поэтому я публикую свой ответ здесь. Это всего лишь модификация @jib 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">