简体   繁体   English

在JavaScript中通过取消操作来管理复杂事件序列的实用/优雅方法是什么?

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

I have a JavaScript ( EmberJS + Electron ) application that needs to execute sequences of asynchronous tasks. 我有一个JavaScript( EmberJS + Electron )应用程序,需要执行异步任务序列。 Here is a simplified example: 这是一个简化的示例:

  1. Send a message to a remote device 发送消息到远程设备
  2. Receive response less than t1 seconds later 不到t1秒后收到回复
  3. Send another message 传送其他讯息
  4. Receive second response less than t2 seconds later 不到t2秒后收到第二个响应
  5. Display success message 显示成功消息

For simple cases this seems reasonably easy to implement with Promises : 1 then 2 then 3 ... It gets a little trickier when timeouts are incorporated, but Promise.race and Promise.all seem like reasonable solutions for that. 对于简单的情况,使用Promises似乎很容易实现:1,然后2,然后3 ...当合并超时时,这会变得有些棘手,但是Promise.racePromise.all似乎都是解决此问题的合理方法。

However, I need to allow users to be able to cancel a sequence gracefully, and I am struggling to think of sensible ways to do this. 但是,我需要允许用户能够优雅地取消序列,并且我正在努力思考这样做的明智方法。 The first thing that came to mind was to do some kind of polling during each step to see if a variable someplace has been set to indicate that the sequence should be canceled. 首先想到的是在每个步骤中进行某种轮询,以查看是否已在某个位置设置了变量以指示应取消该序列。 Of course, that has some serious problems with it: 当然,这有一些严重的问题:

  • Inefficient: most of the polling time is wasted 效率低下:浪费了大部分轮询时间
  • Unresponsive: an extra delay is introduced by having to poll 无响应:必须轮询才能引入额外的延迟
  • Smelly : I think it goes without saying that this would be inelegant. 臭味 :我认为这毋庸置疑。 A cancel event is completely unrelated to time so shouldn't require using a timer. cancel事件与时间完全无关,因此不需要使用计时器。 The isCanceled variable may need to be outside of the promise' scope. isCanceled变量可能需要超出promise的范围。 etc. 等等

Another thought I had was to perhaps race everything so far against another promise that only resolves when the user sends a cancel signal. 我的另一个想法是,也许到目前为止,所有事情都与另一个仅在用户发送取消信号时才解决的承诺相冲突。 A major problem here is that the individual tasks running (that the user wants to cancel) don't know that they need to stop, roll-back, etc. so even though the code that gets the promise resolution from the race works fine, the code in the other promises does not get notified. 这里的一个主要问题是,正在运行的单个任务(用户要取消)不知道它们需要停止,回滚等,因此即使从竞争中获得承诺解决方案的代码运行正常,其他诺言中的代码不会得到通知。

Once upon a time there was talk about cancel-able promises , but it looks to me like the proposal was withdrawn so won't be incorporated into ECMAScript any time soon though I think the BlueBird promise library supports this idea. 曾几何时,人们谈论 可取消的诺言 ,但在我看来,该提案已撤回,因此尽管我认为BlueBird诺言库支持此想法,但也不会很快纳入ECMAScript中。 The application I'm making already includes the RSVP promise library , so I didn't really want to bring in another one but I guess that's a potential option. 我正在制作的应用程序已经包含RSVP Promise库 ,因此我并不是真的想引入另一个 ,但是我想这是一个潜在的选择。

How else can this problem be solved? 还有什么可以解决这个问题的呢? Should I be using promises at all? 我应该完全使用诺言吗? Would this be better served by a pub/sub event system or some such thing? 通过发布/订阅事件系统或诸如此类的事情可以更好地解决此问题?

Ideally, I'd like to separate the concern of being canceled from each task (just like how the Promise object is taking care of the concern of asynchronicity). 理想情况下,我想将被取消的关注与每个任务分开(就像Promise对象如何处理异步的关注一样)。 It'd also be nice if the cancellation signal could be something passed-in/injected. 如果取消信号可以是传入/注入的信号,那也很好。

Despite not being graphically skilled, I've attempted to illustrate what I'm trying to do by making the two drawings below. 尽管没有图形技能,但我还是尝试通过制作下面的两张图来说明我正在尝试做的事情。 If you find them confusing then feel free to ignore them. 如果您发现它们令人困惑,请随时忽略它们。

时间线显示事件顺序


该图显示了事件的可能顺序

If I understand your problem correctly, the following may be a solution. 如果我正确理解您的问题,则可能是以下解决方案。

Simple timeout 简单超时

Assume your mainline code looks like this: 假设您的主线代码如下所示:

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

receive would be something like: receive将类似于:

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

This assumes the existence of an underlying API receiveMessage , which takes two callbacks as parameters, one for success and one for failure. 假定存在底层API receiveMessage ,该API将两个回调作为参数,一个用于成功,一个用于失败。 receive simply wraps receiveMessage with the addition of the timeout which rejects the promise if time t passes before receiveMessage resolves. receive只是封装receiveMessage通过添加其拒绝许超时如果时间t过去之前receiveMessage解析。

User cancellation 用户取消

But how to structure this so that an external user can cancel the sequence? 但是如何构造它以便外部用户可以取消序列? You have the right idea to use a promise instead of polling. 您有使用承诺而不是轮询的正确想法。 Let's write our own cancelablePromise : 让我们编写我们自己的cancelablePromise

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

We pass an "executor" and a "canceler". 我们通过了“执行人”和“取消人”。 "Executor" is the technical term for the parameter passed to the Promise constructor, a function with the signature (resolve, reject) . “执行程序”是传递给Promise构造函数的参数的技术术语,该函数带有签名(resolve, reject) The canceler we pass in is a promise, which when fulfilled, cancels (rejects) the promise we are creating. 我们传入的取消器是一个诺言,当实现时,它取消(拒绝)我们正在创建的诺言。 So cancelablePromise works exactly like new Promise , with the addition of a second parameter, a promise for use in canceling. 因此cancelablePromise工作原理与new Promise完全一样,只是增加了第二个参数,即用于取消的承诺。

Now you can write your code as something like the following, depending on when you want to be able to cancel: 现在,您可以根据需要取消的时间,按如下所示编写代码:

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

If you are programming in ES6 and like using classes, you could write 如果您正在ES6中编程并且喜欢使用类,则可以编写

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

This would then obviously be used as in 这显然将用于

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

If programming in TypeScript, with the above code you will likely need to target ES6 and run the resulting code in an ES6-friendly environment which can handle the subclassing of built-ins like Promise correctly. 如果使用TypeScript进行编程,则使用上述代码,您可能需要针对ES6,并在对ES6友好的环境中运行生成的代码,该环境可以正确处理诸如Promise之类的内置子类。 If you target ES5, the code TypeScript emits might not work. 如果以ES5为目标,则TypeScript发出的代码可能不起作用。

The above approach has a minor (?) defect. 上面的方法有一个轻微的(?)缺陷。 Even if canceler has fulfilled before we start the sequence, or invoke cancelablePromise(receiveMessage, canceler1) , although the promise will still be canceled (rejected) as expected, the executor will nevertheless run, kicking off the receiving logic--which in the best case might consume network resources we would prefer not to. 即使我们开始序列之前 cancelablePromise(receiveMessage, canceler1) canceler已经实现,或者调用cancelablePromise(receiveMessage, canceler1) ,尽管诺言仍将按预期被取消(拒绝),但是执行器仍将运行,开始接收逻辑-最好这种情况可能会消耗我们不希望的网络资源。 Solving this problem is left as an exercise. 解决这个问题留作练习。

"True" cancelation “真实”取消

But none of the above addresses what may be the real issue: to cancel an in-progress asynchronous computation. 但是上述方法都没有解决真正的问题:取消正在进行的异步计算。 This kind of scenario was what motivated the proposals for cancelable promises, including the one which was recently withdrawn from the TC39 process. 这种情况促使人们提出了取消承诺的提议,包括最近从TC39流程中撤回的提议。 The assumption is that the computation provides some interface for cancelling it, such as xhr.abort() . 假定该计算提供了一些取消它的接口,例如xhr.abort()

Let's assume that we have a web worker to calculate the n th prime, which kicks off on receiving the go message: 假设我们有一个网络工作者来计算第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

We can make this cancelable, assuming the worker responds to a "stop" message to terminate its work, again using a canceler promise, by doing: 我们可以把这个取消,假设工人响应一个"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}`);
    });
  }
}

The same approach could be used for a network request, where the cancellation would involve calling xhr.abort() , for example. 相同的方法可以用于网络请求,例如,取消将涉及调用xhr.abort()

By the way, one rather elegant (?) proposal for handling this sort of situation, namely promises which know how to cancel themselves, is to have the executor, whose return value is normally ignored, instead return a function which can be used to cancel itself. 顺便说一句,用于处理这种情况的一个相当优雅的提案(?),即知道如何取消自身的诺言,是让执行者返回其通常被忽略的返回值,而返回一个可以用来取消的函数本身。 Under this approach, we would write the findPrime executor as follows: 在这种方法下,我们将如下编写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'}));
}

In other words, we need only to make a single change to the executor: a return statement which provides a way to cancel the computation in progress. 换句话说,我们只需要对执行程序进行一次更改即可: return语句,它提供了一种取消正在进行的计算的方法。

Now we can write a generic version of cancelablePromise , which we will call cancelablePromise2 , which knows how to work with these special executors that return a function to cancel the process: 现在我们可以编写一个通用版本的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}`));
    });

  });
}

Assuming a single canceler, your code can now be written as something like 假设有一个取消器,您的代码现在可以写成类似

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

At the moment that the user clicks on the "Cancel" button, and the canceler promise is fulfilled, any pending sends will be canceled, with the worker stopping in midstream, and/or any pending receives will be canceled, and the promise will be rejected, that rejection cascading down the chain to the final catch . 在用户点击“取消”按钮,瞬间canceler承诺的兑现,任何悬而未决的发送将被取消,与工人在中途停止,和/或任何挂起的接收将被取消,并承诺会被拒绝,那拒绝顺着连锁反应顺流而下,直到最后一catch

The various approaches that have been proposed for cancelable promise attempt to make the above more streamlined, more flexible, and more functional. 已经提出了可取消的承诺的各种方法试图使上述内容更加精简,更灵活和更实用。 To take just one example, some of them allow synchronous inspection of the cancellation state. 仅举一个例子,其中一些允许同步检查取消状态。 To do this, some of them use the notion of "cancel tokens" which can be passed around, playing a role somewhat analogous to our canceler promises. 要做到这一点,他们中的一些使用“取消标记”,它可以传递,打有点类似于我们的一个角色的概念canceler承诺。 However, in most cases cancellation logic can be handled without too much complexity in pure userland code, as we have done here. 但是,在大多数情况下,如我们在此处所做的那样,可以在纯用户域代码中处理取消逻辑而不会带来太多复杂性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 是否有实用的方法将JavaScript / HTML用作ActionScript 3.0中的声学回声消除的代理? - Is there a practical way to use JavaScript/HTML as a proxy for acoustic echo cancellation in ActionScript 3.0? Javascript继承使用原型,有什么实用的方法? - Javascript inheritance using prototype, what is a practical way? 什么是切换javascript计时器的更优雅的方式 - what is a more elegant way of toggling a javascript timer 管理Javascript内存中变量的最佳方法是什么 - What is the best way to manage variable in memory for Javascript 使用Javascript / jQuery,将列表呈现到表格中的最优雅的方法是什么? - Using Javascript / jQuery, what is the most elegant way to render a list into a table? 在 JavaScript 构造函数中接受和处理不同选项的最优雅的方式是什么? - What is the most elegant way to accept and handle different options in JavaScript constructor? JavaScript 咖喱:有哪些实际应用? - JavaScript curry: what are the practical applications? 在JavaScript中部分复制对象数组的最优雅方法是什么 - What is the most elegant way of partly copying object arrays in JavaScript 什么是javascript中将字节转换为可读格式的最优雅方式 - What is the most elegant way in javascript to convert bytes into a readable format JavaScript 中的闭包有什么实际用途? - What is a practical use for a closure in JavaScript?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM