取消异步操作
如果操作已被用户取消或已变得多余,则能够停止长时间运行的操作特别有用。 在多线程编程中,我们可以终止线程,但在 Node.js 这样的单线程平台上,事情可能会变得更复杂一些。
在本节中,我们将讨论取消异步操作,而不是取消承诺,这是完全不同的事情。 顺便说一句,Promises/A+ 标准不包含用于取消 Promise 的 API。 但是,如果您需要这样的功能,您可以使用第三方 Promise 库,例如 bluebird(更多信息请参见 nodejsdp.link/bluebird-cancelation)。 请注意,取消 Promise 并不意味着 Promise 引用的操作也将被取消; 事实上,除了resolve和reject之外,bluebird还在promise构造函数中提供了onCancel回调,可以用来在promise被取消时取消底层的异步操作。 这实际上就是本节的内容。 |
创建可取消函数的基本方法
实际上,在异步编程中,取消函数执行的基本原理非常简单:我们在每次异步调用后检查操作是否已被取消,如果是,则提前退出操作。 例如,考虑以下代码:
import {asyncRoutine} from './asyncRoutine.js'
import {CancelError} from './cancelError.js'
async function cancelable(cancelObj) {
const resA = await asyncRoutine('A')
console.log(resA)
if (cancelObj.cancelRequested) {
throw new CancelError()
}
const resB = await asyncRoutine('B')
console.log(resB)
if (cancelObj.cancelRequested) {
throw new CancelError()
}
const resC = await asyncRoutine('C')
console.log(resC)
}
cancelable() 函数接收一个对象 (cancelObj) 作为输入,该对象包含一个名为 cancelRequested 的属性。 在函数中,我们在每次异步调用后检查 cancelRequested 属性,如果是 true,我们会抛出一个特殊的 CancelError 异常来中断函数的执行。
asyncRoutine() 函数只是一个演示函数,它将一个字符串打印到控制台并在 100 毫秒后返回另一个字符串。 您可以在本书的代码存储库中找到它的完整实现以及 CancelError 的实现。
需要注意的是,只有在 cancelable() 函数将控制权交还给事件循环之后(通常是在等待异步操作时),cancelable() 函数外部的任何代码才能够设置 cancelRequested 属性。 这就是为什么只有在异步操作完成后才值得检查 cancelRequested 属性,而不是更频繁地检查。
下面的代码演示了如何取消cancelable()函数:
const cancelObj = {cancelRequested: false}
cancelable(cancelObj)
.catch(err => {
if (err instanceof CancelError) {
console.log('Function canceled')
} else {
console.error(err)
}
})
setTimeout(() => {
cancelObj.cancelRequested = true
}, 100)
正如我们所看到的,要取消该函数,我们只需将 cancelObj.cancelRequested 属性设置为 true 即可。 这将导致函数停止并抛出 CancelError。
包装异步调用
创建和使用基本的异步可取消函数非常容易,但涉及很多样板文件。 事实上,它涉及太多额外的代码,以至于很难识别该功能的实际业务逻辑。
我们可以通过在包装函数中包含取消逻辑来减少样板代码,我们可以使用该函数来调用异步例程。
这样的包装器如下所示(cancelWrapper.js 文件):
import {CancelError} from './cancelError.js'
export function createCancelWrapper() {
let cancelRequested = false
function cancel() {
cancelRequested = true
}
function cancelWrapper(func, ...args) {
if (cancelRequested) {
return Promise.reject(new CancelError())
}
return func(...args)
}
return {cancelWrapper, cancel}
}
我们的包装器是通过名为 createCancelWrapper() 的工厂函数创建的。 工厂返回两个函数:包装函数(cancelWrapper)和触发异步操作取消的函数(cancel)。 这允许我们创建一个包装函数来包装多个异步调用,然后使用单个 cancel() 函数取消所有异步调用。
cancelWrapper() 函数将一个要调用的函数 (func) 和一组要传递给该函数的参数 (args) 作为输入。 包装器只是检查是否已请求取消,如果是肯定的,它将返回一个被拒绝的承诺,并以 CancelError 对象作为拒绝原因; 否则,它将调用 func。
现在让我们看看我们的包装工厂如何极大地提高 cancelable() 函数的可读性和模块化性:
import {asyncRoutine} from './asyncRoutine.js'
import {createCancelWrapper} from './cancelWrapper.js'
import {CancelError} from './cancelError.js'
async function cancelable(cancelWrapper) {
const resA = await cancelWrapper(asyncRoutine, 'A')
console.log(resA)
const resB = await cancelWrapper(asyncRoutine, 'B')
console.log(resB)
const resC = await cancelWrapper(asyncRoutine, 'C')
console.log(resC)
}
const {cancelWrapper, cancel} = createCancelWrapper()
cancelable(cancelWrapper)
.catch(err => {
if (err instanceof CancelError) {
console.log('Function canceled')
} else {
console.error(err)
}
})
setTimeout(() => {
cancel()
}, 100)
我们可以立即看到使用包装函数来实现取消逻辑的好处。 事实上,cancelable() 函数现在更加简洁和可读。
使用生成器可取消的异步函数
与直接在代码中嵌入取消逻辑相比,我们刚刚创建的可取消包装函数已经向前迈出了一大步。 然而,由于两个原因,它仍然不理想:它很容易出错(如果我们忘记包装一个函数怎么办?),它仍然影响代码的可读性,这使得它不适合实现已经很大并且很庞大的可取消异步操作。 复杂的。
一个更巧妙的解决方案涉及使用发电机。 在第 9 章 “行为设计模式” 中,我们介绍了生成器作为实现迭代器的一种方法。 然而,它们是一种非常通用的工具,可用于实现各种算法。 在这种情况下,我们将使用生成器构建一个监督程序来控制函数的异步流程。 结果将是一个可透明取消的异步函数,其行为类似于异步函数,其中 wait 指令被yield 替换。
让我们看看如何使用生成器(createAsyncCancelable.js 文件)实现这个可取消函数:
import {CancelError} from './cancelError.js'
export function createAsyncCancelable(generatorFunction) { // (1)
return function asyncCancelable(...args) {
const generatorObject = generatorFunction(...args) // (3)
let cancelRequested = false
function cancel() {
cancelRequested = true
}
const promise = new Promise((resolve, reject) => {
async function nextStep(prevResult) { // (4)
if (cancelRequested) {
return reject(new CancelError())
}
if (prevResult.done) {
return resolve(prevResult.value)
}
try { // (5)
nextStep(generatorObject.next(await prevResult.value))
} catch (err) {
try { // (6)
nextStep(generatorObject.throw(err))
} catch (err2) {
reject(err2)
}
}
}
nextStep({})
})
return {promise, cancel} // (2)
}
}
createAsyncCancelable() 函数可能看起来很复杂,所以让我们更详细地分析一下:
-
首先,我们应该注意到 createAsyncCancelable() 函数将一个生成器函数(受监督函数)作为输入并返回另一个函数(asyncCancelable( )) 用我们的监督逻辑包装生成器函数。 我们将使用 asyncCancelable() 函数来调用异步操作。
-
asyncCancelable() 函数返回一个具有两个属性的对象: Promise 属性,其中包含表示异步操作的最终解决(或拒绝)的 Promise。 b. cancel 属性,这是一个可用于取消受监督异步流的函数。
-
调用时,asyncCancelable() 的第一个任务是使用作为输入接收的参数 (args) 调用生成器函数并获取生成器对象,我们可以用它来控制正在运行的协程的执行流程。
-
监督程序的整个逻辑在 nextStep() 函数中实现,该函数负责迭代受监督协程 (prevResult) 生成的值。 这些可以是实际的价值观或承诺。 如果请求取消,我们会抛出通常的 CancelError; 否则,如果协程已终止(例如 prevResult.done 为 true),我们立即解析外部 Promise 并完成返回。
-
nextStep() 函数的核心部分是我们检索受监督协程(我们不要忘记,它是一个生成器)产生的下一个值。 我们等待该值,这样我们就可以确保在处理承诺时获得实际的分辨率值。 这也确保了如果 prevResult.value 是一个承诺并且它被拒绝,我们最终会进入 catch 语句。 即使受监督的协程实际上抛出了异常,我们也可能会陷入 catch 语句。
-
在 catch 语句中,我们将捕获的错误抛出到协程内部。 如果协程已经抛出该错误,则这是多余的,但如果它是承诺拒绝的结果,则不是多余的。 即使不是最佳的,为了演示目的,这个技巧也可以稍微简化我们的代码。 我们使用将 nextStep 扔到协程内部后产生的任何值来调用 nextStep() ,但如果结果是另一个异常(例如,协程内部未捕获异常或抛出另一个异常),我们立即拒绝外部 Promise 并完成异步操作。
正如我们所看到的,createAsyncCancelable() 函数中有很多移动部分。 但我们应该欣赏这样一个事实:只需几行代码,我们就能够创建一个不需要任何手动取消逻辑的可取消函数。 正如我们现在将看到的,结果令人印象深刻。
让我们使用在 createAsyncCancelable() 函数中实现的管理程序重写示例异步可取消操作:
import {asyncRoutine} from './asyncRoutine.js'
import {createAsyncCancelable} from './createAsyncCancelable.js'
import {CancelError} from './cancelError.js'
const cancelable = createAsyncCancelable(function* () {
const resA = yield asyncRoutine('A')
console.log(resA)
const resB = yield asyncRoutine('B')
console.log(resB)
const resC = yield asyncRoutine('C')
console.log(resC)
})
const {promise, cancel} = cancelable()
promise.catch(err => {
if (err instanceof CancelError) {
console.log('Function canceled')
} else {
console.error(err)
}
})
setTimeout(() => {
cancel()
}, 100)
我们可以立即看到 createAsyncCancelable() 包装的生成器非常类似于异步函数,但我们使用的是 yield 而不是 await。 而且,根本没有可见的取消逻辑。 生成器函数保留了异步函数的优秀属性(例如,使异步代码看起来像同步代码),但与异步函数不同,并且由于 createAsyncCancelable() 引入的管理程序,它还可以取消操作。
第二个有趣的方面是 createAsyncCancelable() 创建一个函数(称为 cancelable),可以像任何其他函数一样调用它,但同时返回一个表示操作结果的 Promise 和一个取消操作的函数。
这种使用生成器的技术代表了我们实现可取消异步操作的最佳选择。
对于生产中的使用,大多数时候,我们可以依赖 Node.js 生态系统中广泛使用的包,例如 caf(缩写词表示可取消异步流),您可以在 nodejsdp.link/caf 中找到它。 |