Janji - apakah mungkin untuk memaksa membatalkan janji

91

Saya menggunakan ES6 Promises untuk mengelola semua pengambilan data jaringan saya dan ada beberapa situasi di mana saya perlu memaksa membatalkannya.

Pada dasarnya skenario ini sedemikian rupa sehingga saya memiliki pencarian tipe-depan di UI di mana permintaan yang didelegasikan ke backend harus melakukan pencarian berdasarkan input parsial. Meskipun permintaan jaringan ini (# 1) mungkin membutuhkan sedikit waktu, pengguna terus mengetik yang pada akhirnya memicu panggilan backend lain (# 2)

Di sini # 2 secara alami lebih diutamakan daripada # 1 jadi saya ingin membatalkan permintaan pembungkusan Janji # 1. Saya sudah memiliki cache dari semua Promises di lapisan data sehingga saya dapat mengambilnya secara teoritis saat saya mencoba mengirimkan Promise untuk # 2.

Tetapi bagaimana cara membatalkan Janji # 1 setelah saya mengambilnya dari cache?

Adakah yang bisa menyarankan pendekatan?

Moonwalker
sumber
2
apakah itu pilihan untuk menggunakan fungsi debounce yang setara agar tidak memicu permintaan yang sering dan menjadi permintaan usang? Katakanlah penundaan 300 ms akan berhasil. Misalnya Lodash memiliki salah satu implementasi - lodash.com/docs#debounce
shershen
Ini adalah saat hal-hal seperti Bacon dan Rx berguna.
elclanrs
@shershen yes - kami memiliki ini tetapi ini tidak terlalu banyak tentang masalah UI .. permintaan server mungkin membutuhkan sedikit waktu jadi saya ingin dapat membatalkan Janji ...
Moonwalker
Coba Observable from Rxjs
FieryCod

Jawaban:

164

Tidak. Kita belum bisa melakukannya.

ES6 janji tidak mendukung pembatalan belum . Sedang dalam perjalanan, dan desainnya adalah sesuatu yang dikerjakan dengan sangat keras oleh banyak orang. Semantik pembatalan suara sulit dilakukan dengan benar dan ini sedang dalam proses. Ada perdebatan menarik tentang "fetch" repo, esdiscuss dan beberapa repo lain di GH tapi saya hanya akan bersabar jika saya jadi Anda.

Tapi, tapi, tapi .. pembatalan itu sangat penting!

Hal ini, kenyataan yang terjadi adalah pembatalan adalah benar-benar skenario penting dalam pemrograman sisi klien. Kasus yang Anda gambarkan seperti membatalkan permintaan web itu penting dan ada di mana-mana.

Jadi ... bahasanya mengacaukan saya!

Ya, maaf soal itu. Janji harus masuk terlebih dahulu sebelum hal-hal lebih lanjut ditentukan - jadi janji masuk tanpa beberapa hal berguna seperti .finallydan .cancel- meskipun sedang dalam perjalanan, ke spesifikasi melalui DOM. Pembatalan bukanlah renungan, ini hanya kendala waktu dan pendekatan yang lebih berulang untuk desain API.

Jadi apa yang bisa kulakukan?

Anda memiliki beberapa alternatif:

  • Gunakan pustaka pihak ketiga seperti bluebird yang dapat bergerak jauh lebih cepat daripada spesifikasi dan dengan demikian memiliki pembatalan serta banyak hal lainnya - inilah yang dilakukan oleh perusahaan besar seperti WhatsApp.
  • Lulus token pembatalan .

Menggunakan pustaka pihak ketiga cukup jelas. Adapun token, Anda bisa membuat metode Anda mengambil fungsi dan kemudian memanggilnya, seperti:

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

Yang memungkinkan Anda melakukan:

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

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

Kasus penggunaan Anda yang sebenarnya - last

Ini tidak terlalu sulit dengan pendekatan token:

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

Yang memungkinkan Anda melakukan:

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

Dan tidak, perpustakaan seperti Bacon dan Rx tidak "bersinar" di sini karena mereka adalah perpustakaan yang dapat diamati, mereka hanya memiliki keuntungan yang sama dengan perpustakaan janji tingkat pengguna dengan tidak terikat spesifikasi. Saya kira kita akan menunggu untuk memilikinya dan melihat di ES2016 ketika observable menjadi native. Mereka adalah bagus untuk typeahead sekalipun.

Benjamin Gruenbaum
sumber
25
Benjamin, sangat senang membaca jawaban Anda. Dipikirkan dengan sangat baik, terstruktur, pandai bicara, dan dengan contoh serta alternatif praktis yang baik. Sangat membantu. Terima kasih.
Moonwalker
Token pembatalan @FranciscoPresencia sedang dalam proses sebagai proposal tahap 1.
Benjamin Gruenbaum
Di mana kita bisa membaca tentang pembatalan berbasis token ini? Dimana lamarannya?
membahayakan
@harm proposal sudah mati pada tahap 1.
Benjamin Gruenbaum
1
Saya suka karya Ron, tapi saya rasa kita harus menunggu sebentar sebelum membuat rekomendasi untuk perpustakaan yang belum digunakan orang:] Terima kasih untuk tautannya meskipun saya akan memeriksanya!
Benjamin Gruenbaum
24

Proposal standar untuk janji yang dapat dibatalkan telah gagal.

Promise bukanlah permukaan kontrol untuk tindakan asinkron yang memenuhinya; membingungkan pemilik dengan konsumen. Sebagai gantinya, buat fungsi asinkron yang dapat dibatalkan melalui beberapa token yang diteruskan.

Janji lain membuat token yang bagus, membuat pembatalan mudah diterapkan dengan Promise.race:

Contoh: Gunakan Promise.raceuntuk membatalkan efek rantai sebelumnya:

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

Di sini kami "membatalkan" penelusuran sebelumnya dengan memasukkan undefinedhasil dan mengujinya, tetapi "CancelledError"sebagai gantinya kami dapat dengan mudah membayangkan menolaknya .

Tentu saja ini tidak benar-benar membatalkan pencarian jaringan, tetapi itu adalah batasannya fetch. Jika fetchmengambil janji pembatalan sebagai argumen, maka itu bisa membatalkan aktivitas jaringan.

Saya telah mengusulkan "Batalkan pola janji" ini pada es-diskusikan, tepatnya untuk menyarankan agar fetchmelakukan ini.

jib
sumber
@jib kenapa menolak modifikasi saya? Saya hanya mengklarifikasi.
allenyllee
8

Saya telah memeriksa referensi Mozilla JS dan menemukan ini:

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

Mari kita lihat:

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

Di sini ada p1, dan p2 yang dimasukkan Promise.race(...)sebagai argumen, ini sebenarnya menciptakan janji penyelesaian baru, yang Anda butuhkan.

nikola-miljkovic.dll
sumber
BAGUS - ini mungkin yang saya butuhkan. Saya akan mencobanya.
Moonwalker
Jika Anda mengalami masalah dengannya, Anda dapat menempelkan kode di sini sehingga saya dapat membantu Anda :)
nikola-miljkovic
5
Sudah mencobanya. Kurang tepat disana. Ini menyelesaikan Janji tercepat ... Saya harus selalu menyelesaikan yang terakhir dikirimkan yaitu tanpa syarat membatalkan Janji lama apa pun ..
Moonwalker
1
Dengan cara ini semua janji lainnya tidak ditangani lagi, Anda tidak dapat benar-benar membatalkan janji.
nikola-miljkovic
Saya mencobanya, janji kedua (satu di mantan ini) jangan biarkan prosesnya keluar :(
morteza ataiy
3

Untuk Node.js dan Electron, saya sangat merekomendasikan menggunakan Promise Extensions for JavaScript (Prex) . Penulisnya, Ron Buckton, adalah salah satu insinyur kunci TypeScript dan juga orang di balik proposal Pembatalan ECMAScript TC39 saat ini . Pustaka ini terdokumentasi dengan baik dan kemungkinan beberapa Prex akan memenuhi standar.

Pada catatan pribadi dan berasal dari latar belakang C #, saya sangat menyukai kenyataan bahwa Prex dimodelkan pada Pembatalan yang ada dalam kerangka Managed Threads , yaitu berdasarkan pendekatan yang diambil dengan CancellationTokenSource/ CancellationToken.NET API. Menurut pengalaman saya, itu sangat berguna untuk menerapkan logika pembatalan yang kuat di aplikasi yang dikelola.

Saya juga memverifikasinya untuk berfungsi dalam browser dengan menggabungkan Prex menggunakan Browserify .

Berikut adalah contoh penundaan dengan pembatalan ( Gist dan RunKit , menggunakan Prex untuk CancellationTokendan 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}`));

Perhatikan bahwa pembatalan adalah perlombaan. Yaitu, sebuah janji mungkin telah berhasil diselesaikan, tetapi pada saat Anda mematuhinya (dengan awaitatau then), pembatalan mungkin telah dipicu juga. Terserah Anda bagaimana Anda menangani perlombaan ini, tetapi tidak ada salahnya untuk meminta token.throwIfCancellationRequested()waktu tambahan, seperti yang saya lakukan di atas.

noseratio
sumber
1

Saya menghadapi masalah serupa baru-baru ini.

Saya memiliki klien berbasis janji (bukan jaringan) dan saya ingin selalu memberikan data terbaru yang diminta kepada pengguna untuk menjaga UI tetap lancar.

Setelah berjuang dengan ide pembatalan, Promise.race(...)dan Promise.all(..)saya baru saja mulai mengingat id permintaan terakhir saya dan ketika janji terpenuhi saya hanya memberikan data saya ketika itu cocok dengan id permintaan terakhir.

Semoga bisa membantu seseorang.

Igor Słomski
sumber
Slomski pertanyaannya bukan tentang apa yang akan ditampilkan di UI. Ini tentang membatalkan janji
CyberAbhay
0

Anda bisa membuat janji menolak sebelum menyelesaikan:

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

Sayangnya panggilan pengambilan telah dilakukan, jadi Anda akan melihat panggilan tersebut diselesaikan di tab Jaringan. Kode Anda akan mengabaikannya.

Rashomon
sumber
0

Menggunakan subclass Promise yang disediakan oleh paket eksternal, ini dapat dilakukan sebagai berikut: Demo langsung

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
Dmitriy Mozgovoy
sumber
-1

Karena @jib menolak modifikasi saya, maka saya memposting jawaban saya di sini. Ini hanya modifikasi jawaban @ jib dengan beberapa komentar dan menggunakan nama variabel yang lebih mudah dimengerti.

Di bawah ini saya hanya menunjukkan contoh dari dua metode berbeda: satu adalah menyelesaikan () yang lainnya adalah menolak ()

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

allenyllee
sumber