Hàm setTimeout() trong js

Theo e hiểu thì kết quả sẽ chạy ra là từ 0 đến 4 và mỗi lần chạy in ra kết quả thì sau 1s mới in,
Tại sao kết quả lại ra là 5 lần 5 ạ ? Em có đọc trên mạng về callback function mà vẫn chưa hiểu,
Anh chị nào thông não chi tiết hộ em với ạ,

for (var i = 0; i < 5; i++) {
  setTimeout(function(){ 
    console.log('Yo! ', i);
  }, 1000);
}
var _loop_1 = function (i) {
    setTimeout(function () {
        console.log('Yo! ', i);
    }, 1000);
};
for (var i = 0; i < 5; i++) {
    _loop_1(i);
}
1 Like

Cái này gọi là Closure nhé bạn :smiley:

Sử dụng closure bên trong 1 vòng lặp

Closure là 1 chủ đề thường thấy trong các buổi phỏng vấn JavaScript, nó giúp người phỏng vấn đánh giá bạn thành thục ngôn ngữ đến đâu, bạn biết implement closure hay không.

Về cơ bản, closure là 1 hàm nội truy cập đến các biến bên ngoài phạm vi của nó. Closure có thể được sử dụng để implement privacy và tạo ra các function factory. Một câu hỏi phỏng vấn thường thấy về việc sử dụng closure sẽ có kiểu thế này:

Viết 1 function lặp qua 1 danh sách các số dạng integer và in ra index của mỗi giá trị sau thời gian chờ 3s.

Dưới đây là cách tôi hay thấy khi mọi người giải quyết bài toán này (thực ra là sai):

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
  setTimeout(function() {
    console.log('The index of this number is: ' + i);
  }, 3000);
}

Nếu bạn chạy đoạn code trên, output bạn nhận được sẽ luôn là 4, mặc dù ta mong rằng output sẽ phải là 0, 1, 2, 3 sau mỗi 3s.

Tại sao lại như thế? Để hiểu rõ lý do, đương nhiên bạn cần nắm vững kiến thức về closure của JavaScript, bởi vì người phỏng vấn đang kiểm tra bạn về nó cơ mà!

Lý do là bởi vì hàm setTimeout sẽ tạo ra 1 function (closure) có thể truy cập phạm vi bên ngoài nó, vòng loop sẽ chứa index i. Sau 3s, hàm được thực thi và nó sẽ log ra giá trị của i, là giá trị cuối cùng của vòng lặp (4).

Có 1 số cách để viết hàm đúng. Dưới đây tôi sẽ chỉ nêu ra 2 cách:

const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
  // pass in the variable i so that each function 
  // has access to the correct index
  setTimeout(function(i_local) {
    return function() {
      console.log('The index of this number is: ' + i_local);
    }
  }(i), 3000);
}

const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
  // using the ES6 let syntax, it creates a new binding
  // every single time the function is called
  // read more here: http://exploringjs.com/es6/ch_variables.html#sec_let-const-loop-heads
  setTimeout(function() {
    console.log('The index of this number is: ' + i);
  }, 3000);
}

Ref: https://www.topitworks.com/blogs/phong-van-javascript/

4 Likes

Bởi vì hàm setTimeout nó sẽ thực hiện một hàm mà được chỉ định sau t ms tính từ lúc gọi.
Bạn for 5 lần thì i cuối cùng sẽ có giá trị = 5 tuy nhiên hàm for chạy rất nhanh và nó sẽ thực hiện xong trước tất cả các hàm mà bạn set timeout chưa kịp chạy. Đến khi các hàm chỉ định bởi setTime chạy thì hàm for đã thực hiện xong rồi và tất nhiên lúc này chỉ thu được i =5.

3 Likes

Đọc cuốn YDKJS để hiểu rõ về internals của JS, chính xác hơn là ở đây.

tl;dr: Bởi vì for(var i = ..., ivar nên scope của i sẽ là toàn bộ function nơi vòng lặp được chạy (hoặc global scope nếu ở ngoài). Mỗi lần gọi setTimeout() thì callback bên trong sẽ reference tới cùng biến i đó => khi vòng lặp kết thúc và callback được chạy thì tất cả sẽ đều in ra 5.

Để fix thì chỉ cần bắt callback trỏ tới 1 bản i duy nhất của nó, khỏi phải xài chùa i nữa.

for(var i = 0; i < 5; ++i) {
    (function(x) {
        setTimeout(function () { console.log(x); }, 1000);
    })(i);
}
4 Likes

cám ơn các bác, để em tìm hiểu thêm về closure

Ko phải closure mà là event loop mới giải đáp được câu hỏi của em.

Xem cái này nhé

2 Likes

oh, ok anh. Em còn mơ hồ về cái này quá, :sweat_smile: tình hình là phải còn cày js dài dai :smiley:

Đây là mô phỏng JS runtime environment

Đây là code của em anh viết lại dạng đơn giản hơn:

for (var i = 0; i < 2; i++) {
setTimeout(function(){ console.log(i);}, 1000);}

Các bước thực thi sẽ như sau:

  1. JS engine gặp lệnh vòng lặp for nó gán biến i = 0
  2. Trong vòng lặp nó chạy setTimeout.
  3. setTimeout được thực hiện bởi Web API như trên hình và đặt 1s để chạy hàm function()…
  4. Tuy nhiên JS engine ko đợi setTimeout, mà nó tiếp tục chạy phần chương trình ngoài setTimeout và ở đây phần tiếp theo của chương trình là chạy vòng lặp lần thứ hai.
  5. Lần chạy thứ hai của vòng lặp for, JS engine gán biến i = 1, rồi lặp lại các bước từ 2 đến 4
  6. Sau vòng lặp thứ hai, i lúc này là 1 và chương trình coi như kết thúc
  7. Thế nhưng sau khoảng 1 giây, setTimeout hết thời gian chờ, nó quẳng function(){ console.log(i);}, 1000) vào Callback queue để đẩy lên Call Stack thực hiện.
  8. function()… in ra console giá trị của i, nhưng ̣i lúc này đã bằng 1 như ở bước 6.
  9. Tương tư như thế với setTimeout thứ 2

Mô tả ở trên có thể gọi là cơ chế hoạt động của JS Event Loop

4 Likes

anh nhiệt tình quá. cái e cứ thắc mắc là tại sao i = 0 < 5 sao nó không in ra 0 sau 1 giây mà phải chạy đến tận 5 mới in. giờ thì giác ngộ rồi :smiley:. cảm ơn anh lần nữa

1 Like

Để trả lời chính xác và tường minh nhất câu hỏi này của bạn, hãy tham khảo link sau đây và bạn sẽ hiểu ra được vấn đề ngay sau khi đọc nó: https://github.com/getify/You-Dont-Know-JS/blob/master/scope%20%26%20closures/ch5.md

2 Likes

Giống như @quaangle, vấn đề này chủ yếu thuộc về Event Loop. Quyển Closure chỉ giới thiệu khái niệm lexical scope trong việc truy xuất biến i trong callback.

Nguồn tham khảo cũng từ YDKJS, chỉ cần xem Chapter 1: Asynchrony: Now & Later, quyển thứ 5 - Async & Performance.


Ngoài cách thực thi mà @quaangle đã chỉ, mình viết thêm đoạn code mô phỏng lại hoạt động của Event Loop.

Code trong file do user viết được wrap lại trong hàm run(), parameter setCallback được truyền tự động khi thực hiện event loop.

function run(setCallback) {
  for (var i = 0; i < 5; i++) {
    setCallback(() => console.log(i));
  }
}

Sau đó truyền function run() vào eventLoop(), là function mô phỏng hoạt động của event loop của JS Runtime, nhưng được đơn giản đi.

eventLoop(run);

Dưới đây là phần code của eventLoop():

  • cbsQueue: là vùng nhớ queue chứa các callback ban đầu do user viết (run()), và các callbacks khác phát sinh trong quá trình thực thi code của user.

  • setCallback(cb): là function, có nhiệm vụ đẩy các callbacks phát sinh vào cbsQueue, có các loại callback:

    • Time: setTimeout(), setInterval().
    • OS: fs.read(), fs.write(),…
    • Networking: http.get(), http.post(),…
  • while: lấy từng callback từ cbsQueue rồi thực thi nó, nếu cbsQueue rỗng thì chờ đến khi cbsQueue nhận được callback mới. (Do đó, condition của while là sai, nhưng vì demo nên viết đơn giản)

function eventLoop(userCb) {
  var cbsQueue = [];

  function setCallback(cb) {
    cbsQueue.push(cb);
  }
  
  cbsQueue.push(userCb);
 
  while (cbsQueue.length) {
    var cb = cbsQueue.shift();
    cb(setCallback);
  }
}

Output

5
5
5
5
5
2 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?