簡體   English   中英

在JavaScript中通過取消操作來管理復雜事件序列的實用/優雅方法是什么?

[英]What is a practical / elegant way to manage complex event sequences with cancellation in JavaScript?

我有一個JavaScript( EmberJS + Electron )應用程序,需要執行異步任務序列。 這是一個簡化的示例:

  1. 發送消息到遠程設備
  2. 不到t1秒后收到回復
  3. 傳送其他訊息
  4. 不到t2秒后收到第二個響應
  5. 顯示成功消息

對於簡單的情況,使用Promises似乎很容易實現:1,然后2,然后3 ...當合並超時時,這會變得有些棘手,但是Promise.racePromise.all似乎都是解決此問題的合理方法。

但是,我需要允許用戶能夠優雅地取消序列,並且我正在努力思考這樣做的明智方法。 首先想到的是在每個步驟中進行某種輪詢,以查看是否已在某個位置設置了變量以指示應取消該序列。 當然,這有一些嚴重的問題:

  • 效率低下:浪費了大部分輪詢時間
  • 無響應:必須輪詢才能引入額外的延遲
  • 臭味 :我認為這毋庸置疑。 cancel事件與時間完全無關,因此不需要使用計時器。 isCanceled變量可能需要超出promise的范圍。 等等

我的另一個想法是,也許到目前為止,所有事情都與另一個僅在用戶發送取消信號時才解決的承諾相沖突。 這里的一個主要問題是,正在運行的單個任務(用戶要取消)不知道它們需要停止,回滾等,因此即使從競爭中獲得承諾解決方案的代碼運行正常,其他諾言中的代碼不會得到通知。

曾幾何時,人們談論 可取消的諾言 ,但在我看來,該提案已撤回,因此盡管我認為BlueBird諾言庫支持此想法,但也不會很快納入ECMAScript中。 我正在制作的應用程序已經包含RSVP Promise庫 ,因此我並不是真的想引入另一個 ,但是我想這是一個潛在的選擇。

還有什么可以解決這個問題的呢? 我應該完全使用諾言嗎? 通過發布/訂閱事件系統或諸如此類的事情可以更好地解決此問題?

理想情況下,我想將被取消的關注與每個任務分開(就像Promise對象如何處理異步的關注一樣)。 如果取消信號可以是傳入/注入的信號,那也很好。

盡管沒有圖形技能,但我還是嘗試通過制作下面的兩張圖來說明我正在嘗試做的事情。 如果您發現它們令人困惑,請隨時忽略它們。

時間線顯示事件順序


該圖顯示了事件的可能順序

如果我正確理解您的問題,則可能是以下解決方案。

簡單超時

假設您的主線代碼如下所示:

send(msg1)
  .then(() => receive(t1))
  .then(() => send(msg2))
  .then(() => receive(t2))
  .catch(() => console.log("Didn't complete sequence"));

receive將類似於:

function receive(t) {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject("timed out"), t);
    receiveMessage(resolve, reject);
  });
}

假定存在底層API receiveMessage ,該API將兩個回調作為參數,一個用於成功,一個用於失敗。 receive只是封裝receiveMessage通過添加其拒絕許超時如果時間t過去之前receiveMessage解析。

用戶取消

但是如何構造它以便外部用戶可以取消序列? 您有使用承諾而不是輪詢的正確想法。 讓我們編寫我們自己的cancelablePromise

function cancelablePromise(executor, canceler) {
  return new Promise((resolve, reject) => {
    canceler.then(e => reject(`cancelled for reason ${e}`));
    executor(resolve, reject);
  });
}

我們通過了“執行人”和“取消人”。 “執行程序”是傳遞給Promise構造函數的參數的技術術語,該函數帶有簽名(resolve, reject) 我們傳入的取消器是一個諾言,當實現時,它取消(拒絕)我們正在創建的諾言。 因此cancelablePromise工作原理與new Promise完全一樣,只是增加了第二個參數,即用於取消的承諾。

現在,您可以根據需要取消的時間,按如下所示編寫代碼:

var canceler1 = new Promise(resolve => 
  document.getElementById("cancel1", "click", resolve);
);

send(msg1)
  .then(() => cancelablePromise(receiveMessage, canceler1))
  .then(() => send(msg2))
  .then(() => cancelablePromise(receiveMessage, canceler2))
  .catch(() => console.log("Didn't complete sequence"));

如果您正在ES6中編程並且喜歡使用類,則可以編寫

class CancelablePromise extends Promise {
  constructor(executor, canceler) {
    super((resolve, reject) => {
      canceler.then(reject);
      executor(resolve, reject);
    }
}

這顯然將用於

send(msg1)
  .then(() => new CancelablePromise(receiveMessage, canceler1))
  .then(() => send(msg2))
  .then(() => new CancelablePromise(receiveMessage, canceler2))
  .catch(() => console.log("Didn't complete sequence"));

如果使用TypeScript進行編程,則使用上述代碼,您可能需要針對ES6,並在對ES6友好的環境中運行生成的代碼,該環境可以正確處理諸如Promise之類的內置子類。 如果以ES5為目標,則TypeScript發出的代碼可能不起作用。

上面的方法有一個輕微的(?)缺陷。 即使我們開始序列之前 cancelablePromise(receiveMessage, canceler1) canceler已經實現,或者調用cancelablePromise(receiveMessage, canceler1) ,盡管諾言仍將按預期被取消(拒絕),但是執行器仍將運行,開始接收邏輯-最好這種情況可能會消耗我們不希望的網絡資源。 解決這個問題留作練習。

“真實”取消

但是上述方法都沒有解決真正的問題:取消正在進行的異步計算。 這種情況促使人們提出了取消承諾的提議,包括最近從TC39流程中撤回的提議。 假定該計算提供了一些取消它的接口,例如xhr.abort()

假設我們有一個網絡工作者來計算第n個素數,該素數在收到go消息后開始:

function findPrime(n) {
  return new Promise(resolve => {
    var worker = new Worker('./find-prime.js');
    worker.addEventListener('message', evt => resolve(evt.data));
    worker.postMessage({cmd: 'go', n});
  }
}

> findPrime(1000000).then(console.log)
< 15485863

我們可以把這個取消,假設工人響應一個"stop"消息終止其工作,再次使用canceler承諾,這樣做:

function findPrime(n, canceler) {
  return new Promise((resolve, reject) => {
    // Initialize worker.
    var worker = new Worker('./find-prime.js');

    // Listen for worker result.
    worker.addEventListener('message', evt => resolve(evt.data));

    // Kick off worker.
    worker.postMessage({cmd: 'go', n});

    // Handle canceler--stop worker and reject promise.
    canceler.then(e => {
      worker.postMessage({cmd: 'stop')});
      reject(`cancelled for reason ${e}`);
    });
  }
}

相同的方法可以用於網絡請求,例如,取消將涉及調用xhr.abort()

順便說一句,用於處理這種情況的一個相當優雅的提案(?),即知道如何取消自身的諾言,是讓執行者返回其通常被忽略的返回值,而返回一個可以用來取消的函數本身。 在這種方法下,我們將如下編寫findPrime執行程序:

const findPrimeExecutor = n => resolve => {
  var worker = new Worker('./find-prime.js');
  worker.addEventListener('message', evt => resolve(evt.data));
  worker.postMessage({cmd: 'go', n});

  return e => worker.postMessage({cmd: 'stop'}));
}

換句話說,我們只需要對執行程序進行一次更改即可: return語句,它提供了一種取消正在進行的計算的方法。

現在我們可以編寫一個通用版本的cancelablePromise ,我們將其稱為cancelablePromise2 ,它知道如何與這些特殊的執行程序一起使用,這些執行程序返回一個函數來取消進程:

function cancelablePromise2(executor, canceler) {
  return new Promise((resolve, reject) => {
    var cancelFunc = executor(resolve, reject);

    canceler.then(e => {
      if (typeof cancelFunc === 'function') cancelFunc(e);
      reject(`cancelled for reason ${e}`));
    });

  });
}

假設有一個取消器,您的代碼現在可以寫成類似

var canceler = new Promise(resolve => document.getElementById("cancel", "click", resolve);

function chain(msg1, msg2, canceler) {
  const send = n => () => cancelablePromise2(findPrimeExecutor(n), canceler);
  const receive =   () => cancelablePromise2(receiveMessage, canceler);

  return send(msg1)()
    .then(receive)
    .then(send(msg2))
    .then(receive)
    .catch(e => console.log(`Didn't complete sequence for reason ${e}`));
}

chain(msg1, msg2, canceler);

在用戶點擊“取消”按鈕,瞬間canceler承諾的兌現,任何懸而未決的發送將被取消,與工人在中途停止,和/或任何掛起的接收將被取消,並承諾會被拒絕,那拒絕順着連鎖反應順流而下,直到最后一catch

已經提出了可取消的承諾的各種方法試圖使上述內容更加精簡,更靈活和更實用。 僅舉一個例子,其中一些允許同步檢查取消狀態。 要做到這一點,他們中的一些使用“取消標記”,它可以傳遞,打有點類似於我們的一個角色的概念canceler承諾。 但是,在大多數情況下,如我們在此處所做的那樣,可以在純用戶域代碼中處理取消邏輯而不會帶來太多復雜性。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM