在JavaScript中,什么是一种实用/优雅的方法来管理复杂的事件序列并取消?
我有一个JavaScript(+)应用程序,需要执行异步任务序列。以下是一个简化的示例:在JavaScript中,什么是一种实用/优雅的方法来管理复杂的事件序列并取消?,javascript,events,promise,cancellation,Javascript,Events,Promise,Cancellation,我有一个JavaScript(+)应用程序,需要执行异步任务序列。以下是一个简化的示例: 向远程设备发送消息 在小于t1秒后收到响应 再发一条消息 在小于t2秒后接收第二个响应 显示成功消息 对于简单的情况,这似乎相当容易实现:1,然后2,然后3。。。当超时被合并时,它变得有点棘手,但似乎是合理的解决方案 然而,我需要允许用户能够优雅地取消一个序列,我正在努力想出合理的方法来做到这一点。想到的第一件事是在每个步骤中进行某种轮询,以查看是否设置了一个变量someplace来指示应该取消序列。当然,
t1
秒后收到响应t2
秒后接收第二个响应- 低效:大部分轮询时间都被浪费了
- 无响应:由于必须进行轮询,会导致额外的延迟
- :我认为这是不雅的,这是不言而喻的。
事件与时间完全无关,因此不需要使用计时器。cancel
变量可能需要超出承诺的范围。等等isCanceled
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);
});
}
这假设存在一个底层APIreceiveMessage
,它接受两个回调作为参数,一个表示成功,一个表示失败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);