gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-11-30 09:39:57 25 4
gpt4 key购买 nike

我有一个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/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com