Обещание - можно ли отменить обещание
Я использую ES6 Promises для управления всеми моими сетевыми данными, и есть ситуации, когда мне нужно принудительно отменить их.
В основном сценарий таков, что у меня есть поиск по принципу "вперед" в пользовательском интерфейсе, где запрос делегирован на бэкэнд, чтобы выполнить поиск на основе частичного ввода. Хотя этот сетевой запрос (№ 1) может занять немного времени, пользователь продолжает вводить код, который в конечном итоге вызывает другой бэкэнд-вызов (# 2)
Здесь # 2, естественно, имеет приоритет над # 1, поэтому я хотел бы отменить запрос об упаковке Promise # 1. У меня уже есть кеш всех Promises в слое данных, поэтому я могу теоретически получить его, поскольку я пытаюсь отправить Promise для # 2.
Но как мне отменить Promise # 1, как только я извлечу его из кеша?
Может ли кто-нибудь предложить подход?
Ответы
Ответ 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(..)
я только начал запоминать свой последний идентификатор запроса, и когда обещание было выполнено, я рендерил свои данные только тогда, когда они соответствовали идентификатору последнего запроса,
Надеюсь, это поможет кому-то.
Ответ 6
Смотрите https://www.npmjs.com/package/promise-abortable
$ npm install promise-abortable
Ответ 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">