在JavaScript中,什么是一种实用/优雅的方法来管理复杂的事件序列并取消?

在JavaScript中,什么是一种实用/优雅的方法来管理复杂的事件序列并取消?,javascript,events,promise,cancellation,Javascript,Events,Promise,Cancellation,我有一个JavaScript(+)应用程序,需要执行异步任务序列。以下是一个简化的示例: 向远程设备发送消息 在小于t1秒后收到响应 再发一条消息 在小于t2秒后接收第二个响应 显示成功消息 对于简单的情况,这似乎相当容易实现:1,然后2,然后3。。。当超时被合并时,它变得有点棘手,但似乎是合理的解决方案 然而,我需要允许用户能够优雅地取消一个序列,我正在努力想出合理的方法来做到这一点。想到的第一件事是在每个步骤中进行某种轮询,以查看是否设置了一个变量someplace来指示应该取消序列。当然,

我有一个JavaScript(+)应用程序,需要执行异步任务序列。以下是一个简化的示例:

  • 向远程设备发送消息
  • 在小于
    t1
    秒后收到响应
  • 再发一条消息
  • 在小于
    t2
    秒后接收第二个响应
  • 显示成功消息
  • 对于简单的情况,这似乎相当容易实现:1,然后2,然后3。。。当超时被合并时,它变得有点棘手,但似乎是合理的解决方案

    然而,我需要允许用户能够优雅地取消一个序列,我正在努力想出合理的方法来做到这一点。想到的第一件事是在每个步骤中进行某种轮询,以查看是否设置了一个变量someplace来指示应该取消序列。当然,这也有一些严重的问题:

    • 低效:大部分轮询时间都被浪费了
    • 无响应:由于必须进行轮询,会导致额外的延迟
    • :我认为这是不雅的,这是不言而喻的。
      cancel
      事件与时间完全无关,因此不需要使用计时器。
      isCanceled
      变量可能需要超出承诺的范围。等等
    我的另一个想法是,目前为止,可能所有事情都与另一个只有在用户发送取消信号时才能解决的承诺相悖。这里的一个主要问题是,正在运行的单个任务(用户希望取消)不知道它们需要停止、回滚等。因此,即使从竞争中获得承诺解析的代码工作正常,其他承诺中的代码也不会得到通知

    曾经有过,但在我看来,提案被撤回了,所以不会很快被纳入ECMAScript中,尽管我认为他们支持这个想法。我正在制作的应用程序已经包含了,所以我真的不想再引入一个,但我想这是一个潜在的选择

    除此之外,如何解决此问题? 我应该使用承诺吗?酒吧/酒吧活动系统或类似的系统会更好地服务于此吗

    理想情况下,我希望将被取消的问题从每个任务中分离出来(就像
    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
    ,它接受两个回调作为参数,一个表示成功,一个表示失败
    receive
    简单地包装
    receiveMessage
    ,添加超时,如果时间
    t
    receiveMessage
    解析之前过去,则拒绝承诺

    用户取消 但是,如何构造该序列,以便外部用户可以取消该序列?你有正确的想法使用承诺而不是投票。让我们写下自己的
    可取消的承诺

    function cancelablePromise(executor, canceler) {
      return new Promise((resolve, reject) => {
        canceler.then(e => reject(`cancelled for reason ${e}`));
        executor(resolve, reject);
      });
    }
    
    我们通过一个“执行者”和一个“取消者”。“Executor”是传递给Promise构造函数的参数的技术术语,该函数的签名为
    (resolve,reject)
    。我们传递的取消者是一个承诺,当它实现时,取消(拒绝)我们正在创造的承诺。因此,
    cancelablePromise
    的工作原理与
    newpromise
    完全相同,只添加了第二个参数,即用于取消的承诺

    现在,您可以按照如下方式编写代码,具体取决于您希望何时取消:

    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发出的代码可能无法工作

    上述方法有一个小缺陷。即使
    canceler
    在启动序列之前履行了,或调用
    cancelablePromise(receiveMessage,canceler1)
    ,尽管承诺仍将按预期被取消(拒绝),执行者仍将运行,启动接收逻辑——在最好的情况下,这可能会消耗我们不希望消耗的网络资源。解决这个问题只剩下练习了

    “真实”取消 但上述任何一项都不能解决真正的问题:取消正在进行的
    异步计算。正是这种情况推动了可撤销承诺的提议,包括最近从TC39进程中撤回的承诺。假设计算提供了一些用于取消它的接口,例如
    xhr.abort()

    假设我们有一个web工作程序来计算第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
    
    同样的方法也可以用于网络请求,例如,在网络请求中取消将涉及调用
    xhr.abort()

    顺便说一句,处理这种情况的一个相当优雅的建议,即知道如何取消自己的承诺,是让执行者(其返回值通常被忽略)返回一个可以
    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}`);
        });
      }
    }
    
    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'}));
    }
    
    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);