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

标签 javascript events promise cancellation

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


发送消息到远程设备
不到t1秒后收到响应
传送其他讯息
不到t2秒后收到第二个响应
显示成功消息


对于简单的情况,使用Promises似乎很容易实现:1然后2然后3 ...当合并了超时时,它会变得有些棘手,但是Promise.racePromise.all似乎是合理的解决方案。

但是,我需要允许用户能够优雅地取消序列,并且我正在努力思考这样做的明智方法。首先想到的是在每个步骤中进行某种轮询,以查看是否已在某个位置设置了变量以指示应取消该序列。当然,这有一些严重的问题:


效率低下:浪费了大部分轮询时间
无响应:必须轮询才能引入额外的延迟
Smelly:我认为这无疑是不雅的。 cancel事件与时间完全无关,因此不需要使用计时器。 isCanceled变量可能需要超出promise的范围。等等


我的另一个想法是,也许到目前为止,所有事情都与另一个仅在用户发送取消信号时才解决的承诺相冲突。这里的主要问题是,正在运行的单个任务(用户要取消)不知道它们需要停止,回滚等,因此即使从竞争中获得承诺解决方案的代码都能正常工作,其他诺言中的代码不会得到通知。

很久以前就有talk about cancel-able promises,但是在我看来,该提案已撤回,因此尽管我认为BlueBird promise library支持该想法,但也不会很快纳入ECMAScript。我正在制作的应用程序已经包含RSVP promise library,因此我并不是真的想引入另一个,但是我想这是一个潜在的选择。

还有什么可以解决这个问题的呢?
我应该完全使用诺言吗?通过发布/订阅事件系统或诸如此类的事情可以更好地解决此问题?

理想情况下,我想将被取消的关注从每个任务中分离出来(就像Promise对象如何处理异步的关注一样)。如果取消信号可以是传入/注入的信号,那也很好。

尽管没有图形技能,但我还是尝试通过制作下面的两张图来说明我正在尝试做的事情。如果您发现它们令人困惑,请随时忽略它们。

Time line showing event sequencing



Diagram showing possible sequences of events

最佳答案

如果我正确理解您的问题,则可能是以下解决方案。

简单超时

假设您的主线代码如下所示:

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发出的代码可能不起作用。

上面的方法有一个轻微的(?)缺陷。即使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承诺。但是,在大多数情况下,如我们在此处所做的那样,可以在纯用户域代码中处理取消逻辑而不会带来太多复杂性。

关于javascript - 在JavaScript中通过取消操作来管理复杂事件序列的实用/优雅方法是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41625444/

相关文章:

javascript - event.currentTarget 在 Chrome 上未定义

javascript - 在每次执行时增加 setInterval 计时器

javascript - Socket.io console.log 多次

javascript - 使用 stopPropagation() 处理 React 事件委托(delegate)

php - 如何使用 AWS SDK PHP 将触发器添加到 AWS Lambda 函数?

javascript - 未捕获( promise )SyntaxError : Unexpected end of input after json fetch

javascript - 递归 promise 不返回

javascript - 将 JSON 中的大数字解析为字符串

java - 在一个类中重写多个类的多个方法

javascript - 为什么 then() 链式方法没有按顺序运行?