Söz - bir sözü iptal etmeye zorlamak mümkün mü


91

Tüm ağ verisi alımımı yönetmek için ES6 Promises kullanıyorum ve bunları iptal etmeye zorlamam gereken bazı durumlar var.

Temel olarak senaryo, isteğin arka uca delege edildiği kullanıcı arayüzünde önden yazma aramamın kısmi girdiye dayalı olarak aramayı yürütmesi gerektiği şekildedir. Bu ağ isteği (# 1) biraz zaman alabilse de, kullanıcı yazmaya devam ederek sonunda başka bir arka uç çağrısını (# 2) tetikler

Burada # 2, doğal olarak # 1'den önceliklidir, bu yüzden Söz sarma isteğini # 1 iptal etmek istiyorum. Veri katmanında tüm Promises'in bir önbelleğine zaten sahibim, böylece # 2 için bir Promise sunmaya çalışırken teorik olarak geri alabilirim.

Ama önbellekten aldıktan sonra Söz 1'i nasıl iptal ederim?

Biri bir yaklaşım önerebilir mi?


2
Bu, sık sık tetiklenmeyen ve istenmeyen istek haline gelmeyen bir geri çevirme işlevinin eşdeğerini kullanma seçeneği midir? Diyelim ki 300 ms gecikme işe yarayacaktır. Örneğin, Lodash uygulamalardan birine sahiptir - lodash.com/docs#debounce
shershen

Bu, Bacon ve Rx gibi şeylerin işe yaradığı zamandır.
elclanrs

@shershen evet - buna sahibiz ama bu kullanıcı arayüzü sorunuyla ilgili değil ... sunucu sorgusu biraz zaman alabilir, bu yüzden Sözleri iptal edebilmek istiyorum ...
Moonwalker


Rxjs'den Gözlemlenebilirleri Deneyin
FieryCod

Yanıtlar:


164

Hayır. Bunu henüz yapamayız.

ES6 vaatleri henüz iptali desteklemiyor . Yolda ve tasarımı birçok insanın üzerinde gerçekten çok çalıştığı bir şey. Ses iptali anlambilimini doğru yapmak zordur ve bu devam eden bir çalışmadır. "Getirme" deposu, esdispus ve GH üzerindeki diğer birkaç repo hakkında ilginç tartışmalar var ama senin yerinde olsam sabırlı olurdum.

Ama, ama, ama .. iptal gerçekten önemli!

İşin gerçeği, iptalin gerçekten istemci tarafı programlamada önemli bir senaryo olmasıdır. Web isteklerini iptal etmek gibi tanımladığınız durumlar önemlidir ve her yerdedir.

Yani ... dil beni mahvetti!

Evet, bunun için üzgünüm. Daha fazla şey belirtilmeden önce vaatlerin girmesi gerekiyordu - bu yüzden .finallyve gibi bazı yararlı şeyler olmadan girdiler .cancel- yine de, spesifikasyonlara DOM aracılığıyla geliyor. İptal, sonradan akla gelen bir şey değildir , sadece bir zaman kısıtlaması ve API tasarımına daha yinelemeli bir yaklaşımdır.

Öyleyse ne yapabilirim?

Birkaç alternatifiniz var:

  • Spesifikasyondan çok daha hızlı hareket edebilen ve bu nedenle iptal edilebilen ve diğer birçok güzelliğe sahip olan bluebird gibi bir üçüncü taraf kitaplığı kullanın - WhatsApp gibi büyük şirketlerin yaptığı şey budur.
  • Bir iptal jetonu iletin .

Üçüncü taraf bir kitaplık kullanmak oldukça açıktır. Bir belirteçle ilgili olarak, yönteminizin bir işlevi almasını ve ardından aşağıdaki gibi çağırmasını sağlayabilirsiniz:

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;
   });
};

Hangisi yapmanıza izin verir:

var token = {};
var promise = getWithCancel("/someUrl", token);

// later we want to abort the promise:
token.cancel();

Gerçek kullanım durumunuz - last

Belirteç yaklaşımında bu çok zor değil:

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);
    };
}

Hangisi yapmanıza izin verir:

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
});

Ve hayır, Bacon ve Rx gibi kütüphaneler burada "parlamaz" çünkü bunlar gözlemlenebilir kütüphanelerdir, sadece kütüphanelerin spesifikasyona bağlı olmadıkları için kullanıcı seviyesinde vaat ettikleri avantaja sahiptirler. Sanırım ES2016'da gözlemlenebilirler doğal hale geldiğinde görmeyi bekleyeceğiz. Yine de , yazı tipi için şıklar.


25
Benjamin, cevabını okumaktan gerçekten zevk aldım. Çok iyi düşünülmüş, yapılandırılmış, ifade edilmiş ve iyi pratik örnekler ve alternatiflerle. Gerçekten yardımcı. Teşekkür ederim.
Moonwalker

@FranciscoPresencia iptal jetonları, 1. aşama teklifi olarak yolda.
Benjamin Gruenbaum

Bu token bazlı iptali nerede okuyabiliriz? Teklif nerede?
zarar

@harm teklif 1. aşamada öldü.
Benjamin Gruenbaum

1
Ron'un çalışmalarını seviyorum, ancak insanların henüz kullanmadığı kütüphaneler için tavsiyelerde bulunmadan önce biraz beklememiz gerektiğini düşünüyorum:] Bağlantı için teşekkürler, yine de bir göz atacağım!
Benjamin Gruenbaum

24

İptal edilebilir vaatler için standart teklifler başarısız oldu.

Bir vaat, onu yerine getiren eşzamansız eylem için bir kontrol yüzeyi değildir; sahibi ile tüketiciyi karıştırır. Bunun yerine, bazı iletilen belirteçlerle iptal edilebilen zaman uyumsuz işlevler oluşturun .

Başka bir vaat, iyi bir jeton oluşturarak iptal işleminin uygulanmasını kolaylaştırır Promise.race:

Örnek:Promise.race Önceki bir zincirin etkisini iptal etmek için kullanın :

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

Burada bir undefinedsonucu enjekte ederek ve test ederek önceki aramaları "iptal ediyoruz" , ancak "CancelledError"bunun yerine reddetmeyi kolayca hayal edebiliriz .

Tabii ki bu aslında ağ aramasını iptal etmez, ancak bu bir sınırlamasıdır fetch. fetchBağımsız değişken olarak bir iptal sözü alırsa , ağ etkinliğini iptal edebilir.

Ben ettik önerilen bu konuda "söz desenini İptal" tam olarak önermek, es-tartışmak fetchbunu.


@jib neden değişikliğimi reddediyorsun? Ben sadece açıklığa kavuşturuyorum.
allenyllee

8

Mozilla JS referansına baktım ve şunu buldum:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race

Hadi kontrol edelim:

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
});

Burada p1 ve p2'yi Promise.race(...)argüman olarak koyduk , bu aslında yeni bir çözüm vaadi yaratıyor, ki buna ihtiyacınız var.


GÜZEL - belki de tam olarak ihtiyacım olan şey bu. Bir deneyeceğim.
Moonwalker

Eğer sorun yaşarsanız, size yardımcı olabilmem için kodu buraya yapıştırabilirsiniz :)
nikola-miljkovic

5
Denedim. Tam orada değil. Bu, en hızlı Sözü çözer ... Her zaman en son gönderilenleri çözmem gerekir, yani daha eski Sözleri kayıtsız şartsız iptal etmeliyim ..
Moonwalker

1
Bu şekilde artık diğer tüm sözler yerine getirilmez, bir sözü gerçekten iptal edemezsiniz.
nikola-miljkovic

Denedim, ikinci söz (bu
örnekte bir tane)

3

Node.js ve Electron için, JavaScript için Promise Extensions (Prex) kullanmanızı şiddetle tavsiye ederim . Yazarı Ron Buckton , en önemli TypeScript mühendislerinden biridir ve aynı zamanda mevcut TC39'un ECMAScript İptal önerisinin arkasındaki kişidir . Kütüphane iyi belgelenmiştir ve Prex'in bazılarının standartlara uygun olma ihtimali vardır.

Kişisel bir not olarak ve C # arka planından geldiğimde, Prex'in Yönetilen İş Parçacıkları çerçevesindeki mevcut İptal üzerine , yani CancellationTokenSource/ CancellationToken.NET API'leri ile alınan yaklaşıma dayalı olarak modellenmesi gerçeğini çok seviyorum . Deneyimlerime göre, bunlar, yönetilen uygulamalarda güçlü iptal mantığı uygulamak için çok kullanışlı oldu.

Ayrıca, Browserify kullanarak Prex'i paketleyerek bir tarayıcıda çalıştığını doğruladım .

İşte iptalli bir gecikme örneği ( Gist ve RunKit , ve için Prex kullanarak ):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}`));

İptalin bir yarış olduğunu unutmayın. Yani, bir söz başarıyla çözülmüş olabilir, ancak onu gözlemlediğinizde ( awaitveya ile then), iptal de tetiklenmiş olabilir. Bu yarışı nasıl idare edeceğiniz size kalmış, ancak token.throwIfCancellationRequested()yukarıda yaptığım gibi fazladan bir süre aramaktan zarar gelmez .


1

Son zamanlarda benzer bir sorunla karşılaştım.

Söze dayalı bir istemcim vardı (bir ağ değil) ve kullanıcı arayüzünü sorunsuz tutmak için kullanıcıya her zaman en son istenen verileri vermek istedim.

İptal fikri ile mücadele, sonra Promise.race(...)ve Promise.all(..)ben sadece son isteğim id hatırlayarak başladı ve vaat yerine getirildi zaman son isteğin kimliği eşleşti zaman ben sadece benim veri işleme edildi.

Umarım birine yardımcı olur.


Slomski'nin sorusu, kullanıcı arayüzünde ne gösterileceğiyle ilgili değil.
Sözü


0

Bitirmeden önce sözün reddedilmesini sağlayabilirsiniz:

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

Maalesef getirme çağrısı zaten yapılmıştır, bu nedenle çağrının çözüldüğünü Ağ sekmesinde göreceksiniz. Kodunuz onu görmezden gelecektir.


0

Harici paket tarafından sağlanan Promise alt sınıfını kullanarak bu şu şekilde yapılabilir: Canlı demo

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 değişikliğimi reddettiği için, cevabımı buraya gönderiyorum. Bu sadece @ jib'in yanıtlayıcısının bazı yorumlarla ve daha anlaşılır değişken adları kullanılarak değiştirilmesi.

Aşağıda sadece iki farklı yöntemin örneklerini gösteriyorum: biri resolüsyon (), diğeri 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">

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.