异步中断-AbortController
- Published on
MDN 文档:
AbortController: https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController
AbortSignal: https://developer.mozilla.org/zh-CN/docs/Web/API/AbortSignal
弃用 fetch 请求
构造函数获取 AbortController 实例对象 controller,获取 controller.signal
传入 fetch 参数,作为 signal。调用 controller.abort()
方法可以弃用(cancel) fetch 请求。效果和 ajax 中 xhr.abort()
一样。
const controller = new AbortController();
const signal = controller.signal;
fetch('url',{signal});
// cancel fetch
controller.abort();
demo-从服务和客户端看 abort
从服务端和客户端两边看 abort 的表现。
建立服务。通过
setTimeout
添加延时让响应不那么快,留出时间 abort。在处理请求后返回前打出日志[request /profile]
const http = require('http') const hostname = '127.0.0.1' const port = 3000 const server = http.createServer((req, res) => { if (req.method === 'GET' && req.url === '/profile') { res.statusCode = 200 res.setHeader('Content-Type', 'application/json') res.setHeader('Access-Control-Allow-Origin', '*') setTimeout(() => { console.info('[request /profile]') res.end( JSON.stringify({ name: 'john', age: 30, email: 'john.doe@example.com', }) ) }, 2000) } else { res.statusCode = 404 res.setHeader('Content-Type', 'text/plain') res.end('404 Not Found') } }) server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`) })
建立客户端页面, 用于发起请求。通过较长时间间隔(大于 2S)和较短时间间隔(小于 2S)点击去看多次请求的结果。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div> <button onclick="refetchProfile()">get profile</button> </div> </body> <script> let controller = new AbortController() async function refetchProfile() { if (controller) { controller.abort('abort!') } controller = new AbortController() console.info('signal', controller.signal) fetch('http://127.0.0.1:3000/profile', { signal: controller.signal }) .then((res) => { console.info('[res]', res) }) .catch((err) => { console.warn('[err]', err) }) } </script> </html>
打开客户端页面操作。
4 次快速连续点击(间隔小于 2 秒)。页面上的请求,前三次状态均为 canceled ,第四次等待 2s 后成功返回数据。
服务端是否收到了这些请求?
请求并没有真正被终止,服务端收到了全部请求。
controller.abort()
所做的可能表达为【客户端弃用请求】更合理。客户端 fetch 被 cancel ,会导致 fetch promise 抛出错误[err] abort!
。点击一次之后过 5 秒再点击, 上一次 fetch 已经完成,再去 abort 是没有效果的。
AbortSignal 对象
controller 的只读属性 signal 获取的是一个的 AbortSignal
对象实例。上面就是将signal: controller.signal
参数传入 fetch 后用来控制 abort fetch 请求。 AbortSignal 对象具有静态方法:
- abort 获取一个已终止的 AbortSignal 实例。
- timeout 返回一个指定时间后自动终止的 AbortSignal 实例。
设定超时
MDN 的例子,将一个 5S 后超时的 AbortSignal 实例传入 fetch 的 signal 参数,用来控制 fetch 请求超时时间 5S,通过 catch 判断错误类型(是超时还是其他)进一步处理(出现超时时 fetch promise 抛出 “TimeoutError” 的错误,以此区分)
try {
const res = await fetch(url, { signal: AbortSignal.timeout(5000) })
const result = await res.blob()
} catch (e) {
if (err.name === 'TimeoutError') {
console.error('Timeout: It took more than 5 seconds to get the result!')
} else if (err.name === 'AbortError') {
console.error('Fetch aborted by user action (browser stop button, closing tab, etc.')
} else if (err.name === 'TypeError') {
console.error('AbortSignal.timeout() method is not supported')
} else {
// A network error, or some other problem.
console.error(`Error: type: ${err.name}, message: ${err.message}`)
}
}
手动"终止"Promise
AbortSignal
对象实例还具有事件, abort
。 通过 addEventListener 添加事件处理,于是可以手动 DIY 出来一个可终止的 Promise。
如下 demo: customPromise 方法是一个耗时 3S 的操作,传入 signal 后添加事件监听。在外部需要终止的时候触发 controller.abort, Promse 内部监听后直接 reject 改变 Promise 状态。
function customPromise(signal) {
return new Promise((resolve, reject) => {
signal.addEventListener('abort', () => {
reject(new Error('操作已被中止'))
})
// 模拟异步操作
setTimeout(() => {
console.info('finish')
resolve('操作成功')
}, 3000)
})
}
const controller = new AbortController()
const promise = customPromise(controller.signal)
// 在 1 秒后中止操作
setTimeout(() => {
controller.abort()
}, 1000)
promise.then((result) => console.log(result)).catch((error) => console.error(error.message))
执行后输出:
操作已被中止
[finish]
Pormise 执行,耗时操作期间手动触发 abort 后 reject 调用,模拟异常终止。