Hướng Dẫn Sử Dụng AbortController và AbortSignal trong JavaScript
AbortController
là cách tiêu chuẩn để hủy bỏ công việc bất đồng bộ trong JavaScript hiện đại. Nó kết hợp với AbortSignal, một đối tượng bạn truyền vào các tác vụ để chúng có thể dừng lại ngay lập tức.
Mục Lục
- Tổng Quan
- API Chính (Hỗ Trợ Lý Do)
- Fetch và Thời Gian Chờ
- Biến Các Hàm của Bạn Thành Có Thể Hủy Bỏ
- Streams và Readers (Trình Duyệt + Node)
- Mẫu React
- Tiện Ích Nhỏ
- Cạm Bẫy Thường Gặp
- Tóm Tắt Nhanh
1. Tổng Quan
- Tạo một controller → truyền
controller.signal
vào công việc bất đồng bộ của bạn. - Gọi
controller.abort(reason?)
để hủy; người tiêu dùng sẽ thấy mộtAbortError
(hoặcsignal.reason
). - Hoạt động với
fetch
, streams, và các hàm của riêng bạn.
javascript
const c = new AbortController()
const resP = fetch('/api/data', { signal: c.signal })
// sau đó...
c.abort('người dùng đã điều hướng đi')
try { await resP } catch (e) { if (e.name === 'AbortError') /* bỏ qua */ }
2. API Chính (Hỗ Trợ Lý Do)
javascript
const c = new AbortController()
const { signal } = c
signal.aborted // boolean
signal.reason // any (lý do tại sao nó bị hủy bỏ)
c.abort(new DOMException('Timeout', 'AbortError'))
// hoặc: c.abort('Người dùng rời khỏi trang')
Mẹo: Nếu bạn truyền một lý do, hãy truyền nó trong các tác vụ của riêng bạn.
fetch
vẫn sẽ từ chối vớiAbortError
.
3. Fetch và Thời Gian Chờ
A) Dễ Nhất: AbortSignal.timeout(ms)
javascript
// Trình duyệt hiện đại & Node 18+
const res = await fetch('/slow', { signal: AbortSignal.timeout(3000) })
B) Bộ Đếm Thời Gian Thủ Công
javascript
const c = new AbortController()
const id = setTimeout(() => c.abort(new DOMException('Timeout', 'AbortError')), 3000)
try {
const res = await fetch('/slow', { signal: c.signal })
// sử dụng res
} catch (e) {
if (e.name !== 'AbortError') throw e
} finally {
clearTimeout(id)
}
C) Các Tiện Ích Đua
javascript
// người thắng sẽ nhận tất cả -> hủy bỏ những người thua
const controllers = [new AbortController(), new AbortController()]
const [a, b] = controllers.map(c => fetch('/mirror', { signal: c.signal }))
const winner = await Promise.any([a, b])
controllers.forEach(c => c.abort('thua cuộc đua'))
4. Biến Các Hàm của Bạn Thành Có Thể Hủy Bỏ
javascript
export function wait(ms, signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(resolve, ms)
const onAbort = () => { clearTimeout(id); reject(new DOMException('Aborted', 'AbortError')) }
if (signal.aborted) return onAbort()
signal.addEventListener('abort', onAbort, { once: true })
})
}
Truyền Lý Do:
javascript
const onAbort = () => reject(signal.reason ?? new DOMException('Aborted', 'AbortError'))
5. Streams và Readers (Trình Duyệt + Node)
javascript
const c = new AbortController()
const res = await fetch('/stream', { signal: c.signal }) // có thể bị hủy bỏ
const reader = res.body.getReader({ signal: c.signal }) // hủy bỏ ảnh hưởng đến việc đọc
// sau đó
c.abort()
Node: fetch
trong Node 18+ cũng hỗ trợ hủy bỏ; đối với streams, các thao tác pipe/reader nên phản ứng với hủy bỏ và đóng tài nguyên.
6. Mẫu React
A) Hủy Bỏ Khi Unmount (và khi thay đổi deps)
javascript
useEffect(() => {
const c = new AbortController()
;(async () => {
try {
const r = await fetch('/api/search?q=' + q, { signal: c.signal })
setData(await r.json())
} catch (e) {
if (e.name !== 'AbortError') console.error(e)
}
})()
return () => c.abort('component đã unmounted hoặc q đã thay đổi')
}, [q])
B) Giá Trị Gõ Mới Nhất Thắng (typeahead)
javascript
const ref = useRef<AbortController | null>(null)
async function onType(v: string) {
ref.current?.abort('superseded')
const c = new AbortController()
ref.current = c
try {
const r = await fetch('/api?q=' + v, { signal: c.signal })
setOptions(await r.json())
} catch (e) { if (e.name !== 'AbortError') console.error(e) }
}
7. Tiện Ích Nhỏ (copy‑paste)
javascript
// tạo một controller tự động hủy sau ms
export const withTimeout = (ms = 5000) => AbortSignal.timeout(ms)
// kết hợp nhiều tín hiệu -> bị hủy nếu BẤT KỲ cái nào bị hủy
export function anySignal(...signals) {
const c = new AbortController()
const onAbort = (s) => c.abort(s.reason ?? new DOMException('Aborted', 'AbortError'))
signals.forEach(s => s.addEventListener('abort', () => onAbort(s), { once: true }))
return c.signal
}
Cách sử dụng:
javascript
const c = new AbortController()
const s = anySignal(c.signal, AbortSignal.timeout(3000))
fetch('/x', { signal: s })
8. Cạm Bẫy Thường Gặp
- Không kết nối tín hiệu → truyền
{ signal }
ở mọi nơi mà tác vụ hỗ trợ. - Quên dọn dẹp → xóa bộ đếm thời gian và gỡ bỏ trình lắng nghe khi hủy (sử dụng
{ once: true }
). - Bỏ qua tất cả lỗi → chỉ bỏ qua
AbortError
; đưa ra các lỗi thực sự. - Tái sử dụng controller toàn cầu → tạo các controller mới cho mỗi hoạt động để tránh hủy nhầm.
- Ghi đè lý do → nếu bạn quan tâm đến lý do, hãy sử dụng
abort(reason)
và đọcsignal.reason
trong mã tùy chỉnh.
9. Tóm Tắt Nhanh
Cần | Thực hiện |
---|---|
Hủy bỏ fetch chậm | fetch(url, { signal: AbortSignal.timeout(ms) }) |
Hủy bỏ khi unmount | Tạo AbortController trong useEffect , hủy trong cleanup |
Hủy bỏ yêu cầu trước (tìm kiếm) | Giữ controller cuối cùng trong ref , hủy trước khi fetch mới |
Hủy bỏ một lô | Chia sẻ một controller giữa các yêu cầu và gọi abort() |
Giữ “tại sao” nó bị hủy | controller.abort('reason'); signal.reason |
Chúc bạn hủy bỏ thành công ✨ Sử dụng AbortController để giữ cho các ứng dụng của bạn nhanh nhẹn, chính xác và không bị rò rỉ bộ nhớ.
Tài liệu tham khảo: MDN Web Docs về AbortController
Thực Hành Tốt Nhất
- Luôn kiểm tra trạng thái của
signal.aborted
trước khi thực hiện các tác vụ. - Sử dụng
try/catch
để xử lýAbortError
một cách hiệu quả. - Truyền lý do cụ thể khi hủy bỏ để dễ dàng gỡ lỗi sau này.
Lưu Ý Quan Trọng
- Đảm bảo bạn không quên xóa các trình lắng nghe và bộ đếm thời gian khi hoàn tất hoặc hủy bỏ.
- Sử dụng các công cụ gỡ lỗi như console log để theo dõi hành vi của quá trình hủy bỏ.
Câu Hỏi Thường Gặp (FAQ)
1. Khi nào tôi nên sử dụng AbortController?
Khi bạn có các tác vụ bất đồng bộ mà bạn muốn có khả năng hủy bỏ, như yêu cầu mạng hoặc các tác vụ dài.
2. Có cách nào khác để hủy bỏ các tác vụ không?
Có, nhưng AbortController
là cách tiêu chuẩn và dễ sử dụng nhất trong JavaScript hiện đại.
3. Tôi có thể hủy bỏ nhiều tác vụ cùng lúc không?
Có, bạn có thể tạo một controller và sử dụng tín hiệu của nó cho nhiều tác vụ.
Kết Luận
Sử dụng AbortController
và AbortSignal
sẽ giúp bạn quản lý tốt hơn các tác vụ bất đồng bộ trong ứng dụng của mình, giữ cho chúng nhẹ nhàng và hiệu quả hơn. Hãy áp dụng những kiến thức này vào dự án của bạn ngay hôm nay!