Menu

Sleep trong TypeScript: cách chờ hoặc tạm dừng thực thi

TypeScript không có sleep dựng sẵn, nhưng chỉ một dòng code là bạn có ngay: một hàm trả về Promise<void> được setTimeout resolve. Tìm hiểu cách await nó, tạm dừng trong vòng lặp, thử lại sau một khoảng chờ, hủy sleep, và vì sao JavaScript không có sleep chặn luồng.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

TypeScript không có sleep dựng sẵn. Hãy viết một hàm trả về Promise<void> mà setTimeout resolve sau khoảng chờ, rồi await nó bên trong một hàm async:

await sleep(300) chỉ tạm dừng main. Chương trình không bị đứng: timer, các hàm async khác và I/O vẫn tiếp tục chạy trong lúc chờ.

Hàm sleep hoạt động thế nào

setTimeout(resolve, ms) lên lịch gọi resolve sau ms mili giây, và gọi resolve sẽ hoàn tất promise. await trên promise đó tạm ngưng hàm async cho đến lúc ấy.

Đối số kiểu <void> rất quan trọng. Một new Promise(...) đứng riêng không suy luận kiểu từ các lời gọi resolve, nên thiếu nó thì sleep sẽ trả về Promise<unknown>. Và void là thứ cho phép gọi resolve mà không truyền giá trị: với Promise<number>, gọi resolve() không có đối số là lỗi biên dịch. Khi hàm có khai báo kiểu trả về, bạn có thể đặt đối số kiểu ở đó:

setTimeout đảm bảo thời gian tối thiểu, không phải thời gian chính xác: callback chạy khi khoảng chờ đã qua và event loop đang rảnh. Hãy chấp nhận chênh lệch vài mili giây.

Sleep trong vòng lặp

await sleep() bên trong vòng lặp for hoặc for...of tạm dừng giữa các lần lặp, đây là cách bạn điều tiết nhịp công việc (giới hạn tần suất, polling, hoạt ảnh trong terminal):

Cách này không chạy được trong forEach: nó bỏ qua promise mà mỗi callback async trả về, nên mọi lần lặp bắt đầu cùng lúc và không gì phải chờ. Hãy dùng vòng lặp for hoặc for...of, như giải thích ở trang async/await.

Thử lại sau một khoảng chờ

Một cách dùng thực tế phổ biến: thử một thao tác, nếu thất bại thì chờ rồi thử lại, mỗi lần tăng gấp đôi thời gian chờ (exponential backoff).

return await fn() bên trong try rất quan trọng: thiếu await, một promise bị reject sẽ được trả về trước khi catch kịp thấy nó.

Sleep có thể hủy

Một sleep bình thường không thể bị ngắt. Để dừng chờ sớm (người dùng nhấn Hủy, chương trình bắt đầu tắt), hãy truyền vào một AbortSignal và xóa timer khi tín hiệu được kích hoạt:

Chương trình thoát ngay sau khi hủy vì timer dài đã bị xóa. Thiếu clearTimeout, Node sẽ tiếp tục chạy cho đến khi hết 5 giây.

Promise timer có sẵn của Node

Node có sẵn phiên bản promise của setTimeout trong module node:timers/promises, nên trong code Node bạn có thể import nó thay vì tự viết hàm hỗ trợ. Nó còn nhận một giá trị để resolve và một AbortSignal:

import { setTimeout as sleep } from "node:timers/promises";

async function main() {
    await sleep(1000);                              // wait one second
    const result = await sleep(500, "done");        // resolves with "done"
    const ac = new AbortController();
    await sleep(10_000, undefined, { signal: ac.signal }); // cancellable
}

Các kiểu đến từ @types/node (npm i -D @types/node). Module này không có trong trình duyệt, nơi hàm hỗ trợ một dòng là lựa chọn dùng được ở mọi môi trường.

Không có sleep chặn luồng

Các ngôn ngữ có luồng (thread) đều có sleep chặn luồng hiện tại. JavaScript chạy code của bạn trên một luồng duy nhất với event loop, nên một sleep chặn luồng sẽ làm đứng mọi thứ khác: không timer nào kích hoạt, không response mạng nào được xử lý, và trong trình duyệt thì trang web ngừng phản hồi. Một vòng lặp chạy liên tục cho đến khi đồng hồ vượt qua một mốc thời gian làm đúng điều đó, và không bao giờ là công cụ phù hợp.

sleep bất đồng bộ là cách việc chờ hoạt động trong JavaScript và TypeScript: hàm bị tạm ngưng, luồng được giải phóng, và hàm tiếp tục khi timer kích hoạt. Atomics.wait có thể chặn luồng thật sự, nhưng chỉ trên một SharedArrayBuffer, và trình duyệt cấm dùng nó trên luồng chính; nó tồn tại để điều phối các worker thread, không phải để tạo độ trễ.

Câu hỏi thường gặp

Làm sao sleep trong TypeScript?

Định nghĩa const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)); rồi gọi await sleep(1000) bên trong một hàm async. Hàm đó tạm dừng khoảng một giây mà không chặn phần còn lại của chương trình.

TypeScript có hàm sleep dựng sẵn không?

Không. TypeScript thêm kiểu cho JavaScript và không có thư viện runtime riêng, còn JavaScript thì không có sleep. Trong Node bạn có thể import một hàm: import { setTimeout as sleep } from "node:timers/promises". Trong trình duyệt, hãy viết hàm hỗ trợ một dòng.

Vì sao sleep cần await?

sleep(1000) trả về một promise ngay lập tức; chỉ await mới tạm dừng hàm async bao quanh cho đến khi promise resolve. Không có await, dòng tiếp theo chạy ngay. await chỉ dùng được trong hàm async (hoặc ở cấp cao nhất của một ES module).

Có thể chặn luồng bằng sleep đồng bộ trong TypeScript không?

Không, với code thông thường. JavaScript chạy trên một luồng với event loop, và một vòng lặp chờ bận sẽ làm đứng mọi thứ: timer, callback mạng, cả trang web. Hãy dùng sleep bất đồng bộ. Atomics.wait có thể chặn một luồng trong Node hoặc trong worker, nhưng nó dùng để điều phối worker ở mức thấp, không phải để tạo độ trễ.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu