Một vài tuần trước, tôi đã tweet câu hỏi phỏng vấn này:
*** Trả lời câu hỏi trong đầu của bạn bây giờ trước khi bạn tiếp tục ***
Khoảng một nửa số câu trả lời cho Tweet là sai. Câu trả lời là KHÔNG PHẢI V8 (hoặc các máy ảo khác)!! Mặc dù nổi tiếng với cái tên “Bộ hẹn giờ JavaScript”, các chức năng như setTimeout
và setInterval
không phải là một phần của thông số kỹ thuật ECMAScript hoặc bất kỳ triển khai công cụ JavaScript nào. Các chức năng hẹn giờ được triển khai bởi các trình duyệt và việc triển khai chúng sẽ khác nhau giữa các trình duyệt khác nhau. Bộ hẹn giờ cũng được triển khai nguyên bản bởi chính thời gian chạy Node.js.
Trong các trình duyệt, các chức năng hẹn giờ chính là một phần của Window
giao diện, có một vài chức năng và đối tượng khác. Giao diện đó làm cho tất cả các phần tử của nó có sẵn trên toàn cầu trong phạm vi JavaScript chính. Đây là lý do tại sao bạn có thể thực hiện setTimeout
trực tiếp trong bảng điều khiển của trình duyệt của bạn.
Trong Node, bộ hẹn giờ là một phần của global
đối tượng, hoạt động tương tự như đối tượng của trình duyệt Window
giao diện. Bạn có thể xem mã nguồn của bộ hẹn giờ trong Node tại đây.
Một số người có thể nghĩ rằng đây là một câu hỏi phỏng vấn tồi – tại sao biết điều này lại quan trọng?! Là một nhà phát triển JavaScript, tôi nghĩ bạn nên biết điều này bởi vì nếu không, đó có thể là dấu hiệu cho thấy bạn không hoàn toàn hiểu cách V8 (và các máy ảo khác) tương tác với trình duyệt và Node.
Chúng ta hãy làm một vài ví dụ và thử thách về chức năng hẹn giờ, phải không?
Cập nhật: Bài viết này hiện là một phần trong “Giới thiệu đầy đủ về Node.js” của tôi.
Bạn có thể đọc phiên bản cập nhật của nó tại đây.
Trì hoãn việc thực hiện một chức năng
Các hàm hẹn giờ là các hàm bậc cao hơn có thể được sử dụng để trì hoãn hoặc lặp lại việc thực thi các hàm khác (mà chúng nhận làm đối số đầu tiên).
Đây là một ví dụ về trì hoãn:
// example1.js
setTimeout(
() => {
console.log('Hello after 4 seconds');
},
4 * 1000
);
Ví dụ này sử dụng setTimeout
để trì hoãn việc in lời chào trong 4 giây. Lập luận thứ hai để setTimeout
là độ trễ (tính bằng ms). Đây là lý do tại sao tôi nhân 4 với 1000 để biến nó thành 4 giây.
Đối số đầu tiên để setTimeout
là chức năng mà việc thực thi sẽ bị trì hoãn.
Nếu bạn thực hiện example1.js
tập tin với node
lệnh, Node sẽ tạm dừng trong 4 giây và sau đó nó sẽ in thông báo chào mừng (và thoát sau đó).
Lưu ý rằng đối số đầu tiên để setTimeout
chỉ là một chức năng thẩm quyền giải quyết. Nó không phải là một chức năng nội tuyến như những gì example1.js
có. Đây là ví dụ tương tự mà không sử dụng hàm nội tuyến:
const func = () => {
console.log('Hello after 4 seconds');
};
setTimeout(func, 4 * 1000);
Vượt qua đối số
Nếu chức năng sử dụng setTimeout
để trì hoãn việc thực thi của nó chấp nhận bất kỳ đối số nào, chúng ta có thể sử dụng các đối số còn lại cho setTimeout
chính nó (sau phần 2 mà chúng ta đã học cho đến nay) để chuyển tiếp các giá trị đối số cho hàm bị trì hoãn.
// For: func(arg1, arg2, arg3, ...)
// We can use: setTimeout(func, delay, arg1, arg2, arg3, ...)
Đây là một ví dụ:
// example2.js
const rocks = who => {
console.log(who + ' rocks');
};
setTimeout(rocks, 2 * 1000, 'Node.js');
Các rocks
chức năng trên, bị trễ 2 giây, chấp nhận một who
lập luận và setTimeout
cuộc gọi chuyển tiếp giá trị “Node.js” như thể who
tranh luận.
thi hành example2.js
với node
lệnh sẽ in ra “Đá Node.js” sau 2 giây.
Thử thách hẹn giờ #1
Sử dụng những gì bạn đã học cho đến nay về setTimeout
in 2 thông báo sau sau độ trễ tương ứng của chúng.
- In tin nhắn “Xin chào sau 4 giây” sau 4 giây
- In tin nhắn “Xin chào sau 8 giây” sau 8 giây.
Hạn chế:
Bạn chỉ có thể xác định một hàm duy nhất trong giải pháp của mình, bao gồm các hàm nội tuyến. Điều này có nghĩa là nhiều setTimeout
các cuộc gọi sẽ phải sử dụng chính xác chức năng tương tự.
Dung dịch
Đây là cách tôi giải quyết thử thách này:
// solution1.js
const theOneFunc = delay => {
console.log('Hello after ' + delay + ' seconds');
};
setTimeout(theOneFunc, 4 * 1000, 4);
setTimeout(theOneFunc, 8 * 1000, 8);
tôi đã làm theOneFunc
nhận được delay
đối số và sử dụng giá trị của điều đó delay
đối số trong tin nhắn được in. Bằng cách này, hàm có thể in các thông báo khác nhau dựa trên bất kỳ giá trị độ trễ nào mà chúng ta chuyển cho nó.
sau đó tôi đã sử dụng theOneFunc
trong hai setTimeout
cuộc gọi, một cuộc gọi kích hoạt sau 4 giây và một cuộc gọi khác kích hoạt sau 8 giây. Cả hai điều này setTimeout
cuộc gọi cũng nhận được một lần thứ 3 lập luận để đại diện cho delay
lập luận cho theOneFunc
.
thực hiện solution1.js
tập tin với node
lệnh sẽ in ra các yêu cầu thử thách, tin nhắn đầu tiên sau 4 giây và tin nhắn thứ hai sau 8 giây.
Lặp lại việc thực hiện một chức năng
Điều gì sẽ xảy ra nếu tôi yêu cầu bạn in một tin nhắn cứ sau 4 giây, mãi mãi?
Trong khi bạn có thể đặt setTimeout
trong một vòng lặp, API hẹn giờ cung cấp setInterval
cũng hoạt động, điều này sẽ hoàn thành yêu cầu làm một việc gì đó mãi mãi.
Đây là một ví dụ về setInterval:
// example3.js
setInterval(
() => console.log('Hello every 3 seconds'),
3000
);
Ví dụ này sẽ in tin nhắn của nó cứ sau 3 giây. thi hành example3.js
với node
lệnh sẽ khiến Node in thông báo này mãi mãi, cho đến khi bạn kết thúc quá trình (với CTRL+C).
Hủy hẹn giờ
Bởi vì việc gọi hàm hẹn giờ lên lịch cho một hành động, nên hành động đó cũng có thể bị hủy trước khi nó được thực thi.
Một cuộc gọi đến setTimeout
trả về một “ID” hẹn giờ và bạn có thể sử dụng ID hẹn giờ đó với một clearTimeout
gọi để hủy hẹn giờ đó. Đây là một ví dụ:
// example4.js
const timerId = setTimeout(
() => console.log('You will not see this one!'),
0
);
clearTimeout(timerId);
Bộ đếm thời gian đơn giản này được cho là kích hoạt sau ms (làm cho nó ngay lập tức), nhưng nó sẽ không xảy ra vì chúng tôi đang nắm bắt
timerId
giá trị và hủy bỏ nó ngay sau đó với một clearTimeout
cuộc gọi.
Khi chúng tôi thực hiện example4.js
với node
lệnh, Nút sẽ không in bất cứ thứ gì và quá trình sẽ thoát ra.
Nhân tiện, trong Node.js, có một cách khác để làm setTimeout
với bệnh đa xơ cứng. API hẹn giờ Node.js có một chức năng khác được gọi là
setImmediate
và về cơ bản nó giống như một setTimeout
với một ms nhưng chúng tôi không phải chỉ định độ trễ ở đó:
setImmediate(
() => console.log('I am equivalent to setTimeout with 0 ms'),
);
Các setImmediate
chức năng không có sẵn trong tất cả các trình duyệt. Đừng sử dụng nó cho mã front-end.
Giống như clearTimeout
Cũng có một clearInterval
chức năng, làm điều tương tự nhưng đối với setInerval
cuộc gọi, và cũng có một clearImmediate
gọi là tốt.
Độ trễ hẹn giờ không phải là một điều được đảm bảo
Trong ví dụ trước, bạn có nhận thấy cách thực hiện một cái gì đó với setTimeout
sau đó ms không có nghĩa là thực thi nó ngay lập tức (sau dòng setTimeout), mà là thực thi nó ngay lập tức sau mọi thứ khác trong tập lệnh (bao gồm cả lệnh gọi ClearTimeout)?
Hãy để tôi làm rõ điểm này với một ví dụ. Đây là một cách đơn giản setTimeout
cuộc gọi sẽ kích hoạt sau nửa giây, nhưng nó sẽ không:
// example5.js
setTimeout(
() => console.log('Hello after 0.5 seconds. MAYBE!'),
500,
);
for (let i = 0; i < 1e10; i++) {
// Block Things Synchronously
}
Ngay sau khi xác định bộ đếm thời gian trong ví dụ này, chúng tôi chặn thời gian chạy một cách đồng bộ với một for
vòng. Các 1e10
Là 1
với 10
số không ở phía trước của nó, vì vậy vòng lặp là một 10
Vòng lặp hàng tỷ tích tắc (về cơ bản mô phỏng một CPU bận rộn). Nút không thể làm gì trong khi vòng lặp này đang tích tắc.
Tất nhiên, đây là một điều rất tồi tệ trong thực tế, nhưng nó sẽ giúp bạn hiểu điều đó ở đây. setTimeout
sự chậm trễ không phải là một điều được đảm bảo, mà là một tối thiểu Điều. Các 500
ms có nghĩa là độ trễ tối thiểu là 500
bệnh đa xơ cứng. Trên thực tế, kịch bản sẽ mất nhiều thời gian hơn để in dòng lời chào của nó. Nó sẽ phải đợi vòng lặp chặn kết thúc trước.
Thử thách hẹn giờ #2
Viết đoạn lệnh để in thông báo “Chào thế giới” mỗi giây, nhưng chỉ 5 lần. Sau 5 lần, tập lệnh sẽ in thông báo “Xong” và để quá trình Node thoát ra.
Hạn chế: Bạn không thể sử dụng một setTimeout
kêu gọi thử thách này.
Gợi ý: Bạn cần một bộ đếm.
Dung dịch
Đây là cách tôi giải quyết vấn đề này:
let counter = 0;
const intervalId = setInterval(() => {
console.log('Hello World');
counter += 1;
if (counter === 5) {
console.log('Done');
clearInterval(intervalId);
}
}, 1000);
tôi bắt đầu một counter
giá trị như và sau đó bắt đầu một
setInterval
gọi chụp id của nó.
Chức năng trì hoãn sẽ in thông báo và tăng bộ đếm mỗi lần. Bên trong chức năng trì hoãn, một if
tuyên bố sẽ kiểm tra xem chúng tôi đang ở 5
lần cho đến bây giờ. Nếu vậy, nó sẽ in “Xong” và xóa khoảng thời gian bằng cách sử dụng intervalId
hằng số. Độ trễ khoảng thời gian là 1000
bệnh đa xơ cứng.
Chính xác thì ai “gọi” các chức năng bị trì hoãn?
Khi bạn sử dụng JavaScript this
từ khóa bên trong một chức năng thông thường, như thế này:
function whoCalledMe() {
console.log('Caller is', this);
}
Giá trị bên trong this
từ khóa sẽ đại diện cho người gọi của chức năng. Nếu bạn xác định chức năng trên bên trong Node REPL, người gọi sẽ là global
vật. Nếu bạn xác định chức năng bên trong bảng điều khiển của trình duyệt, người gọi sẽ là window
vật.
Hãy định nghĩa hàm dưới dạng một thuộc tính trên một đối tượng để làm cho điều này rõ ràng hơn một chút:
const obj = {
id: '42',
whoCalledMe() {
console.log('Caller is', this);
}
};
// The function reference is now: obj.whoCallMe
Bây giờ khi bạn gọi obj.whoCallMe
chức năng sử dụng trực tiếp tham chiếu của nó, người gọi sẽ là obj
đối tượng (được xác định bởi id của nó):

Bây giờ, câu hỏi là, người gọi sẽ là gì nếu chúng ta chuyển tham chiếu của obj.whoCallMe
đến một setTimetout
cuộc gọi?
// What will this print??
setTimeout(obj.whoCalledMe, 0);
Ai sẽ là người gọi trong trường hợp đó?
Câu trả lời là khác nhau dựa trên nơi chức năng hẹn giờ được thực thi. Bạn chỉ đơn giản là không thể phụ thuộc vào người gọi là ai trong trường hợp đó. Bạn mất quyền kiểm soát người gọi vì việc triển khai bộ đếm thời gian sẽ là chức năng gọi hàm của bạn ngay bây giờ. Nếu bạn kiểm tra nó trong Node REPL, bạn sẽ nhận được một Timetout
đối tượng là người gọi:

Lưu ý rằng điều này chỉ quan trọng nếu bạn đang sử dụng JavaScript this
từ khóa bên trong các hàm thông thường. Bạn hoàn toàn không cần lo lắng về người gọi nếu bạn đang sử dụng các hàm mũi tên.
Thử thách hẹn giờ #3
Viết đoạn lệnh in liên tục thông báo “Chào thế giới” với độ trễ khác nhau. Bắt đầu với độ trễ 1 giây và sau đó tăng độ trễ thêm 1 giây mỗi lần. Lần thứ hai sẽ có độ trễ là 2 giây. Lần thứ ba sẽ có độ trễ là 3 giây, v.v.
Bao gồm độ trễ trong thông báo được in. Sản lượng dự kiến trông giống như:
Hello World. 1
Hello World. 2
Hello World. 3
...
Hạn chế: Bạn chỉ có thể sử dụng const
để xác định các biến. bạn không thể sử dụng let
hoặc var
.
Dung dịch
Vì độ trễ là một biến trong thử thách này nên chúng tôi không thể sử dụng setInterval
ở đây, nhưng chúng ta có thể tự tạo khoảng thời gian thực thi bằng cách sử dụng setTimeout
trong một cuộc gọi đệ quy. Hàm được thực thi đầu tiên với setTimeout sẽ tạo một bộ đếm thời gian khác, v.v.
Ngoài ra, vì chúng ta không thể sử dụng let/var, nên chúng ta không thể có bộ đếm để tăng độ trễ trong mỗi lệnh gọi đệ quy, nhưng thay vào đó, chúng ta có thể sử dụng các đối số hàm đệ quy để tăng trong khi gọi đệ quy.
Đây là một cách có thể để giải quyết thách thức này:
const greeting = delay =>
setTimeout(() => {
console.log('Hello World. ' + delay);
greeting(delay + 1);
}, delay * 1000);
greeting(1);
Thử thách hẹn giờ #4
Viết đoạn lệnh in liên tục thông báo “Chào thế giới” với cùng một khái niệm độ trễ khác nhau như thử thách số 3, nhưng lần này, theo nhóm 5 thông báo cho mỗi khoảng thời gian trễ chính. Bắt đầu với độ trễ 100 mili giây cho 5 tin nhắn đầu tiên, sau đó là 200 mili giây cho 5 tin nhắn tiếp theo, sau đó là 300 mili giây, v.v.
Đây là cách tập lệnh nên hoạt động:
- Tại thời điểm 100 mili giây, tập lệnh sẽ bắt đầu in “Xin chào thế giới” và thực hiện điều đó 5 lần với khoảng thời gian 100 mili giây. Thông báo đầu tiên sẽ xuất hiện ở 100ms, thông báo thứ 2 ở 200ms, v.v.
- Sau 5 thông báo đầu tiên, tập lệnh sẽ tăng độ trễ chính lên 200 mili giây. Vì vậy, tin nhắn thứ 6 sẽ được in ở 500ms + 200ms (700ms), tin nhắn thứ 7 sẽ được in ở 900ms, tin nhắn thứ 8 sẽ được in ở 1100ms, v.v.
- Sau 10 thông báo, tập lệnh sẽ tăng độ trễ chính lên 300 mili giây. Vì vậy, tin nhắn thứ 11 nên được in ở 500ms + 1000ms + 300ms (18000ms). Thông báo thứ 12 sẽ được in ở tốc độ 21000 mili giây, v.v.
- Tiếp tục mô hình mãi mãi.
Bao gồm độ trễ trong thông báo được in. Đầu ra dự kiến trông như thế này (không có nhận xét):
Hello World. 100 // At 100ms
Hello World. 100 // At 200ms
Hello World. 100 // At 300ms
Hello World. 100 // At 400ms
Hello World. 100 // At 500ms
Hello World. 200 // At 700ms
Hello World. 200 // At 900ms
Hello World. 200 // At 1100ms
...
Hạn chế: Bạn chỉ có thể sử dụng setInterval
cuộc gọi (không phải setTimeout
) và bạn chỉ có thể sử dụng MỘT câu lệnh if.
Dung dịch
Bởi vì chúng ta chỉ có thể sử dụng setInterval
cuộc gọi, chúng tôi cũng sẽ cần đệ quy ở đây để tăng độ trễ của lần tiếp theo setInterval
cuộc gọi. Ngoài ra, chúng ta cần một câu lệnh if để kiểm soát việc thực hiện điều đó chỉ sau 5 lần gọi hàm đệ quy đó.
Đây là một giải pháp khả thi:
let lastIntervalId, counter = 5;
const greeting = delay => {
if (counter === 5) {
clearInterval(lastIntervalId);
lastIntervalId = setInterval(() => {
console.log('Hello World. ', delay);
greeting(delay + 100);
}, delay);
counter = 0;
}
counter += 1;
};
greeting(100);
Cảm ơn vì đã đọc.
Nếu bạn mới bắt đầu tìm hiểu Node.js, gần đây tôi đã xuất bản một khóa học bước đầu tiên tại Pluralsightxem thử:
