Lời hứa - có thể buộc hủy lời hứa không


91

Tôi sử dụng ES6 Promises để quản lý tất cả việc truy xuất dữ liệu mạng của mình và có một số trường hợp tôi cần phải hủy chúng.

Về cơ bản, kịch bản là tôi có một tìm kiếm nhập trước trên giao diện người dùng nơi yêu cầu được ủy quyền cho chương trình phụ trợ phải thực hiện tìm kiếm dựa trên đầu vào một phần. Mặc dù yêu cầu mạng này (# 1) có thể mất một chút thời gian, người dùng tiếp tục nhập và cuối cùng sẽ kích hoạt một cuộc gọi phụ trợ khác (# 2)

Ở đây # 2 đương nhiên được ưu tiên hơn # 1 vì vậy tôi muốn hủy yêu cầu gói Lời hứa # 1. Tôi đã có bộ nhớ cache của tất cả các Lời hứa trong lớp dữ liệu nên về mặt lý thuyết, tôi có thể truy xuất nó khi tôi đang cố gắng gửi Lời hứa cho # 2.

Nhưng làm cách nào để hủy Promise # 1 khi tôi lấy nó từ bộ nhớ cache?

Bất cứ ai có thể đề xuất một cách tiếp cận?


2
đó có phải là một tùy chọn để sử dụng một số chức năng tương đương với chức năng gỡ lỗi không để kích hoạt thường xuyên và trở thành yêu cầu obsolette không? Giả sử độ trễ 300 ms sẽ thực hiện thủ thuật. Ví dụ: Lodash có một trong các triển khai - lodash.com/docs#debounce
shershen

Đây là lúc những thứ như Bacon và Rx trở nên hữu ích.
elclanrs

@shershen vâng - chúng tôi có điều này nhưng đây không phải là quá nhiều về vấn đề giao diện người dùng.. truy vấn máy chủ có thể mất một chút thời gian vì vậy tôi muốn có thể hủy bỏ Lời hứa ...
Moonwalker


Hãy thử Observables từ Rxjs
FieryCod

Câu trả lời:


164

Không. Chúng tôi chưa thể làm điều đó.

ES6 lời hứa không hỗ trợ hủy bỏ chưa . Nó đang trên đà phát triển và thiết kế của nó là thứ mà rất nhiều người đã làm việc rất chăm chỉ. Khó có thể hiểu đúng ngữ nghĩa của việc hủy âm thanh và điều này đang được tiến hành. Có những cuộc tranh luận thú vị về repo "tìm nạp", trên esdiscuss và trên một số repo khác trên GH nhưng tôi sẽ kiên nhẫn nếu tôi là bạn.

Nhưng, nhưng, nhưng .. hủy bỏ là thực sự quan trọng!

Đó là, thực tế của vấn đề là hủy bỏ thực sự là một kịch bản quan trọng trong lập trình phía máy khách. Các trường hợp bạn mô tả như hủy bỏ yêu cầu web là quan trọng và chúng ở khắp mọi nơi.

Vì vậy, ... ngôn ngữ đã làm tôi say mê!

Vâng, xin lỗi về điều đó. Lời hứa phải có trước khi những thứ khác được chỉ định - vì vậy họ đã đi vào mà không có một số thứ hữu ích như .finally.cancel- mặc dù vậy, nó đang trên đường đến thông số kỹ thuật thông qua DOM. Hủy bỏ không phải là một suy nghĩ sau đó mà chỉ là một hạn chế về thời gian và một cách tiếp cận lặp đi lặp lại nhiều hơn đối với thiết kế API.

Vậy tôi có thể làm gì?

Bạn có một số lựa chọn thay thế:

  • Sử dụng thư viện của bên thứ ba như bluebird có thể di chuyển nhanh hơn rất nhiều so với thông số kỹ thuật và do đó có khả năng hủy bỏ cũng như một loạt các tiện ích khác - đây là những gì các công ty lớn như WhatsApp làm.
  • Chuyển mã thông báo hủy .

Sử dụng thư viện của bên thứ ba là điều khá hiển nhiên. Đối với mã thông báo, bạn có thể đặt phương thức của mình đưa một hàm vào và sau đó gọi nó, chẳng hạn như:

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

Điều đó sẽ cho phép bạn làm:

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

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

Trường hợp sử dụng thực tế của bạn - last

Điều này không quá khó với cách tiếp cận mã thông báo:

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

Điều đó sẽ cho phép bạn làm:

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

Và không, các thư viện như Bacon và Rx không "tỏa sáng" ở đây bởi vì chúng là các thư viện có thể quan sát được, chúng chỉ có cùng lợi thế mà các thư viện hứa hẹn cấp người dùng có được là không bị ràng buộc về đặc điểm kỹ thuật. Tôi đoán chúng ta sẽ chờ đợi và xem trong ES2016 khi nào các vật thể quan sát trở thành bản địa. Tuy nhiên, chúng rất tiện lợi cho người đánh máy.


25
Benjamin, thực sự thích đọc câu trả lời của bạn. Được suy nghĩ rất tốt, có cấu trúc, rõ ràng và với các ví dụ thực tế tốt và các lựa chọn thay thế. Thực sự hữu ích. Cảm ơn bạn.
Moonwalker

Mã thông báo hủy @FranciscoPresencia đang được triển khai dưới dạng đề xuất giai đoạn 1.
Benjamin Gruenbaum

Chúng tôi có thể đọc ở đâu về việc hủy dựa trên mã thông báo này? Đề xuất ở đâu?
hại

@harm đề xuất đã chết ở giai đoạn 1
Benjamin Gruenbaum

1
Tôi yêu công việc của Ron, nhưng tôi nghĩ chúng ta nên đợi một chút trước khi đưa ra đề xuất cho các thư viện mà mọi người chưa sử dụng:] Cảm ơn vì liên kết mặc dù tôi sẽ kiểm tra nó!
Benjamin Gruenbaum

24

Đề xuất tiêu chuẩn cho lời hứa có thể hủy bỏ đã thất bại.

Một lời hứa không phải là bề mặt kiểm soát cho hành động không đồng bộ thực hiện nó; nhầm lẫn giữa chủ sở hữu với người tiêu dùng. Thay vào đó, hãy tạo các hàm không đồng bộ có thể bị hủy thông qua một số mã thông báo đã chuyển vào.

Một lời hứa khác tạo ra một mã thông báo tốt, làm cho việc hủy bỏ dễ dàng thực hiện với Promise.race:

Ví dụ: Sử dụng Promise.raceđể hủy bỏ hiệu ứng của một chuỗi trước đó:

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

Ở đây, chúng tôi đang "hủy bỏ" các tìm kiếm trước đó bằng cách đưa vào một undefinedkết quả và thử nghiệm nó, nhưng "CancelledError"thay vào đó chúng tôi có thể dễ dàng hình dung ra việc từ chối .

Tất nhiên điều này không thực sự hủy bỏ tìm kiếm mạng, nhưng đó là một hạn chế của fetch. Nếu fetchlấy lời hứa hủy làm đối số, thì nó có thể hủy hoạt động mạng.

Tôi đã đề xuất "Mô hình hủy lời hứa" này trên es-thảo luận, chính xác là để đề xuất rằng fetchhãy làm điều này.


@jib tại sao lại từ chối sửa đổi của tôi? Tôi chỉ làm rõ nó.
allenyllee

8

Tôi đã kiểm tra tài liệu tham khảo Mozilla JS và thấy điều này:

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

Hãy cùng kiểm tra nào:

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

Chúng ta có ở đây p1 và p2 được đưa vào Promise.race(...)làm đối số, điều này thực sự đang tạo ra lời hứa giải quyết mới, đó là những gì bạn yêu cầu.


NICE - đây có lẽ chính xác là những gì tôi cần. Tôi sẽ cho nó nó một cơ hội.
Moonwalker

Nếu bạn gặp sự cố với nó, bạn có thể dán mã vào đây để tôi có thể hỗ trợ bạn :)
nikola-miljkovic

5
Thử nó. Không hoàn toàn ở đó. Điều này giải quyết Lời hứa nhanh nhất ... Tôi cần phải luôn giải quyết lời hứa đã gửi mới nhất, tức là hủy bỏ vô điều kiện mọi Lời hứa cũ hơn ..
Moonwalker

1
Bằng cách này, tất cả các lời hứa khác không được xử lý nữa, bạn thực sự không thể hủy một lời hứa.
nikola-miljkovic

Tôi đã thử nó, lời hứa thứ hai (một lời hứa trong ví dụ này) không để quá trình thoát :(
morteza ataiy

3

Đối với Node.js và Electron, tôi thực sự khuyên bạn nên sử dụng Promise Extensions cho JavaScript (Prex) . Tác giả Ron Buckton của nó là một trong những kỹ sư TypeScript chủ chốt và cũng là người đứng sau đề xuất Hủy ECMAScript hiện tại của TC39 . Thư viện được ghi chép đầy đủ và rất có thể một số Prex sẽ đạt tiêu chuẩn.

Trên một lưu ý cá nhân và đến từ nền tảng C #, tôi rất thích thực tế là Prex được mô hình hóa dựa trên khung Hủy bỏ hiện có trong Managed Threads , tức là dựa trên cách tiếp cận được thực hiện với CancellationTokenSource/ CancellationToken.NET API. Theo kinh nghiệm của tôi, chúng rất hữu ích để triển khai logic hủy mạnh mẽ trong các ứng dụng được quản lý.

Tôi cũng đã xác minh nó hoạt động trong trình duyệt bằng cách gói Prex bằng cách sử dụng Browserify .

Đây là một ví dụ về sự chậm trễ khi hủy bỏ ( GistRunKit , sử dụng Prex cho 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}`));

Lưu ý rằng hủy bỏ là một cuộc đua. Tức là, một lời hứa có thể đã được giải quyết thành công, nhưng vào thời điểm bạn quan sát nó (với awaithoặc then), việc hủy bỏ cũng có thể đã được kích hoạt. Cách bạn xử lý cuộc đua này là tùy thuộc vào bạn, nhưng bạn không cần phải gọi token.throwIfCancellationRequested()thêm thời gian, như tôi làm ở trên.


1

Tôi phải đối mặt với vấn đề tương tự gần đây.

Tôi đã có một ứng dụng khách dựa trên lời hứa (không phải mạng) và tôi muốn luôn cung cấp dữ liệu được yêu cầu mới nhất cho người dùng để giữ cho giao diện người dùng hoạt động trơn tru.

Sau khi đấu tranh với ý tưởng hủy Promise.race(...)Promise.all(..)tôi chỉ bắt đầu nhớ id yêu cầu cuối cùng của mình và khi lời hứa được thực hiện, tôi chỉ hiển thị dữ liệu của mình khi nó khớp với id của yêu cầu cuối cùng.

Hy vọng nó sẽ giúp một ai đó.


Slomski câu hỏi không phải là về những gì sẽ hiển thị trên giao diện người dùng. Đó là về việc hủy bỏ lời hứa
CyberAbhay


0

Bạn có thể từ chối lời hứa trước khi kết thúc:

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

Rất tiếc, cuộc gọi tìm nạp đã được thực hiện, vì vậy bạn sẽ thấy cuộc gọi phân giải trong tab Mạng. Mã của bạn sẽ bỏ qua nó.


0

Sử dụng lớp con Promise được cung cấp bởi các gói bên ngoài, điều này có thể được thực hiện như sau: Live 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

Vì @jib từ chối sửa đổi của tôi, vì vậy tôi đăng câu trả lời của mình ở đây. Nó chỉ là sửa đổi của anwser @ jib với một số nhận xét và sử dụng các tên biến dễ hiểu hơn.

Dưới đây tôi chỉ đưa ra các ví dụ về hai phương pháp khác nhau: một là giải quyết () còn lại là từ chối ()

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.