Tại sao setTimeout (fn, 0) đôi khi hữu ích?


872

Gần đây tôi đã gặp phải một lỗi khá khó chịu, trong đó mã đang tải <select>động thông qua JavaScript. Điều này được tải động <select>có một giá trị được chọn trước. Trong IE6, chúng tôi đã có code để sửa chữa các lựa chọn <option>, bởi vì đôi khi các <select>'s selectedIndexgiá trị sẽ không được đồng bộ với các lựa chọn <option>của indexthuộc tính, như sau:

field.selectedIndex = element.index;

Tuy nhiên, mã này đã không hoạt động. Mặc dù trường selectedIndexđược đặt chính xác, chỉ mục sai sẽ bị chọn. Tuy nhiên, nếu tôi mắc kẹt một alert()tuyên bố vào đúng thời điểm, tùy chọn chính xác sẽ được chọn. Nghĩ rằng đây có thể là một vấn đề thời gian nào đó, tôi đã thử một vài thứ ngẫu nhiên mà tôi đã thấy trong mã trước đây:

var wrapFn = (function() {
    var myField = field;
    var myElement = element;

    return function() {
        myField.selectedIndex = myElement.index;
    }
})();
setTimeout(wrapFn, 0);

Và điều này đã làm việc!

Tôi đã có một giải pháp cho vấn đề của mình, nhưng tôi không yên tâm rằng tôi không biết chính xác lý do tại sao điều này khắc phục vấn đề của tôi. Có ai có một lời giải thích chính thức? Tôi đang tránh sự cố trình duyệt nào bằng cách gọi chức năng của mình "sau" bằng cách sử dụng setTimeout()?


2
Hầu hết các câu hỏi mô tả tại sao nó hữu ích. Nếu bạn cần biết lý do tại sao điều này xảy ra - hãy đọc câu trả lời của tôi: stackoverflow.com/a/23747597/1090562
Salvador Dali

18
Philip Roberts giải thích điều này theo cách tốt nhất có thể ở đây trong bài nói chuyện của mình "Vòng lặp sự kiện là cái quái gì?" youtube.com/watch?v=8aGhZQkoFbQ
vasa

Nếu bạn đang vội, đây là một phần của video anh ấy bắt đầu trả lời chính xác câu hỏi: youtu.be/8aGhZQkoFbQ?t=14m54s . Regarldless, toàn bộ video là giá trị xem chắc chắn.
William Hampshire

4
setTimeout(fn)giống như setTimeout(fn, 0), btw.
vsync

Câu trả lời:


827

Trong câu hỏi, đã tồn tại một điều kiện cuộc đua giữa:

  1. Trình duyệt cố gắng khởi tạo danh sách thả xuống, sẵn sàng cập nhật chỉ mục đã chọn và
  2. Mã của bạn để đặt chỉ mục đã chọn

Mã của bạn đã liên tục chiến thắng trong cuộc đua này và cố gắng thiết lập lựa chọn thả xuống trước khi trình duyệt sẵn sàng, có nghĩa là lỗi sẽ xuất hiện.

Cuộc đua này tồn tại bởi vì JavaScript có một luồng thực thi duy nhất được chia sẻ với kết xuất trang. Thực tế, việc chạy JavaScript chặn việc cập nhật DOM.

Cách giải quyết của bạn là:

setTimeout(callback, 0)

Gọi setTimeoutvới một cuộc gọi lại và bằng không vì đối số thứ hai sẽ lên lịch cuộc gọi lại được chạy không đồng bộ , sau thời gian trễ ngắn nhất có thể - sẽ khoảng 10ms khi tab tập trung và luồng thực thi JavaScript không bận.

Do đó, giải pháp của OP là trì hoãn khoảng 10ms, cài đặt chỉ mục được chọn. Điều này đã cho trình duyệt một cơ hội để khởi tạo DOM, sửa lỗi.

Mọi phiên bản của Internet Explorer đều thể hiện những hành vi kỳ quặc và cách giải quyết này đôi khi là cần thiết. Ngoài ra, nó có thể là một lỗi chính hãng trong cơ sở mã của OP.


Xem Philip Roberts nói "Cái quái gì là vòng lặp sự kiện?" để giải thích kỹ hơn.


276
'Giải pháp là "tạm dừng" việc thực thi JavaScript để cho phép các luồng kết xuất bắt kịp.' Không hoàn toàn đúng, những gì setTimeout làm là thêm một sự kiện mới vào hàng đợi sự kiện của trình duyệt và công cụ kết xuất đã có trong hàng đợi đó (không hoàn toàn đúng, nhưng đủ gần) để nó được thực thi trước sự kiện setTimeout.
David Mulder

48
Vâng, đó là một câu trả lời chi tiết hơn và chính xác hơn nhiều. Nhưng của tôi là "đủ chính xác" để mọi người hiểu tại sao trò lừa bịp hoạt động.
staticsan

2
@DavidMulder, có nghĩa là trình duyệt phân tích css và kết xuất trong một luồng khác với luồng thực thi javascript?
jason

8
Không, chúng được phân tích cú pháp theo nguyên tắc trong cùng một luồng, nếu không, một vài dòng thao tác DOM sẽ kích hoạt phản xạ mọi lúc sẽ có ảnh hưởng cực kỳ xấu đến tốc độ thực thi.
David Mulder

30
Video này là lời giải thích tốt nhất về lý do tại sao chúng tôi setTimeout 0 2014.jsconf.eu/speaker/ Kẻ
davibq

665

Lời nói đầu:

Một số câu trả lời khác là đúng nhưng thực tế không minh họa vấn đề đang được giải quyết là gì, vì vậy tôi đã tạo ra câu trả lời này để trình bày minh họa chi tiết đó.

Như vậy, tôi đang đăng một hướng dẫn chi tiết về những gì trình duyệt làm và cách sử dụng setTimeout()giúp . Nó trông dài nhưng thực sự rất đơn giản và dễ hiểu - tôi chỉ làm cho nó rất chi tiết.

CẬP NHẬT: Tôi đã tạo một JSFiddle để thể hiện trực tiếp lời giải thích bên dưới: http://jsfiddle.net/C2YBE/31/ . Rất cám ơn @ThangChung đã giúp khởi động nó.

CẬP NHẬT2: Chỉ trong trường hợp trang web JSFiddle chết hoặc xóa mã, tôi đã thêm mã vào câu trả lời này vào cuối.


CHI TIẾT :

Hãy tưởng tượng một ứng dụng web có nút "làm gì đó" và div kết quả.

Trình onClickxử lý cho nút "làm một cái gì đó" gọi một hàm "LongCalc ()", thực hiện 2 việc:

  1. Làm cho một phép tính rất dài (nói mất 3 phút)

  2. In kết quả tính toán vào div kết quả.

Bây giờ, người dùng của bạn bắt đầu thử nghiệm điều này, nhấp vào nút "làm gì đó" và trang nằm đó dường như không có gì trong 3 phút, họ bồn chồn, nhấp lại vào nút, đợi 1 phút, không có gì xảy ra, nhấp lại vào nút ...

Vấn đề là rõ ràng - bạn muốn có một "Trạng thái" DIV, cho thấy những gì đang xảy ra. Hãy xem cách nó hoạt động.


Vì vậy, bạn thêm DIV "Trạng thái" (ban đầu trống) và sửa đổi onclicktrình xử lý (chức năng LongCalc()) để thực hiện 4 điều:

  1. Đưa trạng thái "Tính toán ... có thể mất ~ 3 phút" vào trạng thái DIV

  2. Làm cho một phép tính rất dài (nói mất 3 phút)

  3. In kết quả tính toán vào div kết quả.

  4. Đưa trạng thái "Tính toán xong" vào trạng thái DIV

Và, bạn vui vẻ đưa ứng dụng cho người dùng để kiểm tra lại.

Họ trở lại với bạn trông rất tức giận. Và giải thích rằng khi họ nhấp vào nút, Trạng thái DIV không bao giờ được cập nhật với trạng thái "Tính toán ..." !!!


Bạn gãi đầu, hỏi xung quanh về StackOverflow (hoặc đọc tài liệu hoặc google) và nhận ra vấn đề:

Trình duyệt đặt tất cả các tác vụ "TODO" của nó (cả tác vụ UI và lệnh JavaScript) do các sự kiện vào một hàng đợi . Và thật không may, vẽ lại DIV "Trạng thái" với giá trị "Tính toán ..." mới là một TODO riêng biệt đi đến cuối hàng đợi!

Đây là bảng phân tích các sự kiện trong quá trình kiểm tra người dùng của bạn, nội dung của hàng đợi sau mỗi sự kiện:

  • Xếp hàng: [Empty]
  • Sự kiện: Nhấp vào nút. Xếp hàng sau sự kiện:[Execute OnClick handler(lines 1-4)]
  • Sự kiện: Thực thi dòng đầu tiên trong trình xử lý OnClick (ví dụ: thay đổi giá trị Trạng thái DIV). Xếp hàng sau sự kiện : [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. Xin lưu ý rằng trong khi các thay đổi DOM xảy ra tức thời, để vẽ lại phần tử DOM tương ứng, bạn cần một sự kiện mới, được kích hoạt bởi thay đổi DOM, đã đi vào cuối hàng đợi .
  • VẤN ĐỀ!!! VẤN ĐỀ!!! Chi tiết giải thích bên dưới.
  • Sự kiện: Thực hiện dòng thứ hai trong xử lý (tính toán). Xếp hàng sau : [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value].
  • Sự kiện: Thực hiện dòng thứ 3 trong trình xử lý (điền kết quả DIV). Xếp hàng sau : [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result].
  • Sự kiện: Thực hiện dòng thứ 4 trong trình xử lý (điền trạng thái DIV với "DONE"). Xếp hàng : [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value].
  • Sự kiện: thực hiện ngụ ý returntừ onclickxử lý phụ. Chúng tôi loại bỏ "Thực thi trình xử lý OnClick" khỏi hàng đợi và bắt đầu thực hiện mục tiếp theo trên hàng đợi.
  • LƯU Ý: Vì chúng tôi đã hoàn thành việc tính toán, 3 phút đã trôi qua cho người dùng. Sự kiện vẽ lại chưa xảy ra !!!
  • Sự kiện: vẽ lại Trạng thái DIV với giá trị "Tính toán". Chúng tôi vẽ lại và đưa nó ra khỏi hàng đợi.
  • Sự kiện: vẽ lại Kết quả DIV với giá trị kết quả. Chúng tôi vẽ lại và đưa nó ra khỏi hàng đợi.
  • Sự kiện: vẽ lại Status DIV với giá trị "Xong". Chúng tôi vẽ lại và đưa nó ra khỏi hàng đợi. Những người xem tinh mắt thậm chí có thể nhận thấy "Trạng thái DIV với giá trị" Tính toán "nhấp nháy trong một phần của một phần triệu giây - SAU KHI TÍNH TOÁN HOÀN TOÀN

Vì vậy, vấn đề tiềm ẩn là sự kiện vẽ lại cho "Trạng thái" DIV được đặt trên hàng đợi ở cuối, SAU sự kiện "thực hiện dòng 2" mất 3 phút, do đó, việc vẽ lại thực tế không xảy ra cho đến khi SAU tính toán được thực hiện.


Để giải cứu đến setTimeout(). Nó giúp như thế nào? Bởi vì bằng cách gọi mã thực thi dài thông qua setTimeout, bạn thực sự tạo ra 2 sự kiện: setTimeouttự thực thi và (do hết thời gian chờ), nhập mục hàng đợi riêng biệt cho mã được thực thi.

Vì vậy, để khắc phục sự cố của bạn, bạn sửa đổi onClicktrình xử lý của mình thành các câu lệnh HAI (trong một hàm mới hoặc chỉ là một khối bên trong onClick):

  1. Đưa trạng thái "Tính toán ... có thể mất ~ 3 phút" vào trạng thái DIV

  2. Thực hiện setTimeout()với thời gian chờ 0 và một cuộc gọi đến LongCalc()chức năng .

    LongCalc()chức năng gần giống như lần trước nhưng rõ ràng không có cập nhật trạng thái "Tính toán ..." DIV như bước đầu tiên; và thay vào đó bắt đầu tính toán ngay lập tức.

Vì vậy, chuỗi sự kiện và hàng đợi trông như thế nào bây giờ?

  • Xếp hàng: [Empty]
  • Sự kiện: Nhấp vào nút. Xếp hàng sau sự kiện:[Execute OnClick handler(status update, setTimeout() call)]
  • Sự kiện: Thực thi dòng đầu tiên trong trình xử lý OnClick (ví dụ: thay đổi giá trị Trạng thái DIV). Xếp hàng sau sự kiện : [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Sự kiện: Thực hiện dòng thứ hai trong trình xử lý (cuộc gọi setTimeout). Xếp hàng sau : [re-draw Status DIV with "Calculating" value]. Hàng đợi không có gì mới trong 0 giây nữa.
  • Sự kiện: Báo thức từ thời gian chờ tắt, 0 giây sau. Xếp hàng sau : [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Sự kiện: vẽ lại Trạng thái DIV với giá trị "Tính toán" . Xếp hàng sau : [execute LongCalc (lines 1-3)]. Xin lưu ý rằng sự kiện vẽ lại này thực sự có thể xảy ra TRƯỚC KHI báo thức kêu, nó cũng hoạt động tốt.
  • ...

Hoan hô! Trạng thái DIV vừa được cập nhật thành "Tính toán ..." trước khi bắt đầu tính toán !!!



Dưới đây là mã mẫu từ JSFiddle minh họa các ví dụ sau: http://jsfiddle.net/C2YBE/31/ :

Mã HTML:

<table border=1>
    <tr><td><button id='do'>Do long calc - bad status!</button></td>
        <td><div id='status'>Not Calculating yet.</div></td>
    </tr>
    <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
        <td><div id='status_ok'>Not Calculating yet.</div></td>
    </tr>
</table>

Mã JavaScript: (Được thực thi onDomReadyvà có thể yêu cầu jQuery 1.9)

function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calculation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});

$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    // This works on IE8. Works in Chrome
    // Does NOT work in FireFox 25 with timeout =0 or =1
    // DOES work in FF if you change timeout from 0 to 500
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});

12
câu trả lời tuyệt vời DVK! Đây là một ý chính minh họa ví dụ của bạn gist.github.com/kumikoda/5552511#file-timeout-html
kumikoda

4
Câu trả lời thực sự hay, DVK. Để dễ hình dung, tôi đã đặt mã đó vào jsfiddle jsfiddle.net/thangchung/LVAaV
thangchung

4
@ThangChung - Tôi đã cố gắng tạo một phiên bản tốt hơn (2 nút, mỗi nút cho mỗi trường hợp) trong JSFiddle. Nó hoạt động như một bản demo trên Chrome và IE nhưng không phải trên FF vì một số lý do - xem jsfiddle.net/C2YBE/31 . Tôi đã hỏi tại sao FF không hoạt động ở đây: stackoverflow.com/questions/20747591/NH
DVK

1
@DVK "Trình duyệt đặt tất cả các tác vụ" TODO "của nó (cả tác vụ UI và lệnh JavaScript) do các sự kiện vào một hàng đợi". Thưa ông, xin vui lòng cung cấp nguồn cho việc này? Các trình duyệt của Imho không có giao diện người dùng (công cụ kết xuất) và chủ đề JS khác nhau .... không có ý định xúc phạm .... chỉ muốn tìm hiểu ..
bhavya_w

1
@bhavya_w Không, mọi thứ xảy ra trên một chuỗi. Đây là lý do một phép tính js dài có thể chặn UI
Seba Kerckhof


23

setTimeout() mua cho bạn một thời gian cho đến khi các phần tử DOM được tải, ngay cả khi được đặt thành 0.

Hãy xem điều này: setTimeout


23

Hầu hết các trình duyệt có một quy trình được gọi là luồng chính, chịu trách nhiệm thực thi một số tác vụ JavaScript, cập nhật giao diện người dùng, ví dụ: vẽ, vẽ lại hoặc chỉnh lại dòng, v.v.

Một số tác vụ thực thi JavaScript và cập nhật giao diện người dùng được xếp hàng vào hàng đợi thông báo của trình duyệt, sau đó được gửi đến luồng chính của trình duyệt để được thực thi.

Khi cập nhật giao diện người dùng được tạo trong khi luồng chính đang bận, các tác vụ sẽ được thêm vào hàng đợi tin nhắn.

setTimeout(fn, 0);thêm phần này fnvào cuối hàng đợi sẽ được thực thi. Nó lên lịch một tác vụ sẽ được thêm vào hàng đợi tin nhắn sau một khoảng thời gian nhất định.


"Mọi tác vụ thực thi JavaScript và cập nhật giao diện người dùng được thêm vào hệ thống hàng đợi sự kiện của trình duyệt, sau đó các tác vụ đó được gửi đến Giao diện người dùng chính của trình duyệt sẽ được thực hiện." .... xin vui lòng?
bhavya_w

4
JavaScript hiệu suất cao (Nicholas Zakas, Stoyan Stefanov, Ross Harmes, Julien Lecomte và Matt Sweeney)
Arley

Downvote cho điều này add this fn to the end of the queue. Quan trọng nhất là nơi chính xác setTimeoutthêm func này, kết thúc chu kỳ vòng lặp này hoặc bắt đầu chu kỳ vòng lặp tiếp theo.
Màu xanh lá cây

19

Có những câu trả lời được nêu lên mâu thuẫn ở đây, và không có bằng chứng thì không có cách nào để biết ai tin. Dưới đây là bằng chứng cho thấy @DVK đúng và @SalvadorDali không chính xác. Các yêu cầu sau:

"Và đây là lý do: không thể có setTimeout với thời gian trễ là 0 mili giây. Giá trị tối thiểu được xác định bởi trình duyệt và nó không phải là 0 mili giây. các trình duyệt hiện đại được thiết lập ở mức 4 mili giây. "

Thời gian chờ tối thiểu 4ms không liên quan đến những gì đang xảy ra. Điều thực sự xảy ra là setTimeout đẩy chức năng gọi lại đến cuối hàng đợi thực hiện. Nếu sau setTimeout (gọi lại, 0) bạn có mã chặn mất vài giây để chạy, thì cuộc gọi lại sẽ không được thực thi trong vài giây, cho đến khi mã chặn kết thúc. Hãy thử mã này:

function testSettimeout0 () {
    var startTime = new Date().getTime()
    console.log('setting timeout 0 callback at ' +sinceStart())
    setTimeout(function(){
        console.log('in timeout callback at ' +sinceStart())
    }, 0)
    console.log('starting blocking loop at ' +sinceStart())
    while (sinceStart() < 3000) {
        continue
    }
    console.log('blocking loop ended at ' +sinceStart())
    return // functions below
    function sinceStart () {
        return new Date().getTime() - startTime
    } // sinceStart
} // testSettimeout0

Đầu ra là:

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033

câu trả lời của bạn không chứng minh một điều. Nó chỉ cho thấy rằng trên máy của bạn trong một tình huống cụ thể máy tính ném cho bạn một số số. Để chứng minh một cái gì đó liên quan, bạn cần nhiều hơn một vài dòng mã và một vài số.
Salvador Dali

6
@SalvadorDali, tôi tin rằng bằng chứng của tôi đủ rõ ràng để hầu hết mọi người hiểu. Tôi nghĩ rằng bạn đang cảm thấy phòng thủ và đã không nỗ lực để hiểu nó. Tôi sẽ rất vui khi cố gắng làm rõ điều đó, nhưng tôi không biết những gì bạn không hiểu. Hãy thử chạy mã trên máy của riêng bạn nếu bạn nghi ngờ kết quả của tôi.
Vladimir Kornea

Tôi sẽ cố gắng làm cho nó rõ ràng lần cuối cùng. Câu trả lời của tôi là làm rõ một số thuộc tính thú vị trong thời gian chờ, khoảng thời gian và được hỗ trợ tốt bởi các nguồn hợp lệ và dễ nhận biết. Bạn đã đưa ra một tuyên bố mạnh mẽ rằng nó sai và chỉ vào đoạn mã của bạn mà vì lý do nào đó bạn đặt tên một bằng chứng. Không có gì sai với đoạn mã của bạn (tôi không chống lại nó). Tôi chỉ nói rằng câu trả lời của tôi không sai. Nó làm rõ một số điểm. Tôi sẽ dừng cuộc chiến này, vì tôi không thể nhìn thấy một điểm.
Salvador Dali

15

Một lý do để làm điều đó là trì hoãn việc thực thi mã thành một vòng lặp sự kiện tiếp theo riêng biệt. Khi trả lời một sự kiện trình duyệt nào đó (ví dụ như nhấp chuột), đôi khi chỉ cần thực hiện các thao tác sau khi sự kiện hiện tại được xử lý. Các setTimeout()cơ sở là cách đơn giản nhất để làm điều đó.

chỉnh sửa ngay bây giờ, đó là năm 2015 tôi cần lưu ý rằng cũng có requestAnimationFrame(), điều này không hoàn toàn giống nhau nhưng nó đủ gần với setTimeout(fn, 0)điều đáng nói.


Đây chính xác là một trong những nơi tôi đã thấy nó được sử dụng. =)
Zaibot

FYI: câu trả lời này đã được hợp nhất tại đây từ stackoverflow.com/questions/4574940/
triệt

2
là để trì hoãn việc thực thi mã thành một vòng lặp sự kiện tiếp theo riêng biệt : làm thế nào để bạn tìm ra một vòng lặp sự kiện tiếp theo ? Làm thế nào để bạn tìm ra một vòng lặp sự kiện hiện tại là gì? Làm thế nào để bạn biết bạn đang ở vòng lặp sự kiện nào?
Màu xanh lá cây

@Green tốt, bạn không, thực sự; thực sự không có khả năng hiển thị trực tiếp vào thời gian chạy JavaScript.
Pointy

requestAnimationFrame đã giải quyết vấn đề tôi gặp phải với IE và Firefox đôi khi không cập nhật giao diện người dùng
David

9

Đây là một câu hỏi cũ với câu trả lời cũ. Tôi muốn thêm một cái nhìn mới về vấn đề này và để trả lời tại sao điều này xảy ra và không phải tại sao nó lại hữu ích.

Vì vậy, bạn có hai chức năng:

var f1 = function () {    
   setTimeout(function(){
      console.log("f1", "First function call...");
   }, 0);
};

var f2 = function () {
    console.log("f2", "Second call...");
};

và sau đó gọi chúng theo thứ tự sau f1(); f2();chỉ để thấy rằng cái thứ hai thực hiện trước.

Và đây là lý do tại sao: không thể có setTimeoutthời gian trễ là 0 mili giây. Các giá trị tối thiểu được xác định bởi các trình duyệt và nó không phải là 0 mili giây. Trong lịch sử các trình duyệt đặt mức tối thiểu này là 10 mili giây, nhưng thông số kỹ thuật HTML5 và các trình duyệt hiện đại đã đặt ở mức 4 mili giây.

Nếu mức lồng nhau lớn hơn 5 và thời gian chờ nhỏ hơn 4, thì tăng thời gian chờ lên 4.

Cũng từ mozilla:

Để thực hiện thời gian chờ 0 ms trong trình duyệt hiện đại, bạn có thể sử dụng window.postMessage () như được mô tả ở đây .

Thông tin PS được lấy sau khi đọc bài viết sau .


1
@ user2407309 Bạn đang đùa à? bạn có nghĩa là đặc tả HTML5 là sai và bạn đúng? Đọc các nguồn trước khi bạn downvote và đưa ra tuyên bố mạnh mẽ. Câu trả lời của tôi dựa trên đặc tả HTML và hồ sơ lịch sử. Thay vì lặp đi lặp lại câu trả lời giải thích hoàn toàn cùng một thứ, tôi đã thêm một cái gì đó mới, một cái gì đó không được hiển thị trong các câu trả lời trước. Tôi không nói rằng đây là lý do duy nhất, tôi chỉ thể hiện một cái gì đó mới.
Salvador Dali

1
Điều này không chính xác: "Và đây là lý do: không thể có setTimeout với thời gian trễ là 0 mili giây." Đó không phải là lý do tại sao. Độ trễ 4ms không liên quan đến lý do tại sao lại setTimeout(fn,0)hữu ích.
Vladimir Kornea

@ user2407309 có thể dễ dàng sửa đổi thành "để thêm vào các lý do được nêu bởi những người khác, điều đó là không thể ....". Vì vậy, thật nực cười khi downvote chỉ vì điều này đặc biệt là nếu câu trả lời của riêng bạn không nói lên điều gì mới. Chỉ cần một chỉnh sửa nhỏ là đủ.
Salvador Dali

10
Salvador Dali: Nếu bạn bỏ qua các khía cạnh cảm xúc của cuộc chiến ngọn lửa vi mô ở đây, có lẽ bạn phải thừa nhận rằng @VladimirKornea là đúng. Đúng là các trình duyệt ánh xạ độ trễ 0ms thành 4ms, nhưng ngay cả khi họ không làm như vậy, kết quả vẫn sẽ giống nhau. Cơ chế lái xe ở đây là mã được đẩy lên hàng đợi, thay vì ngăn xếp cuộc gọi. Hãy xem bài thuyết trình xuất sắc này của JSConf, nó có thể giúp làm rõ vấn đề: youtube.com/watch?v=8aGhZQkoFbQ
hashchange

1
Tôi bối rối về lý do tại sao bạn nghĩ rằng trích dẫn của bạn về mức tối thiểu đủ điều kiện là 4 ms là tối thiểu toàn cầu là 4 ms. Như trích dẫn của bạn từ thông số kỹ thuật HTML5 cho thấy, mức tối thiểu chỉ là 4 ms khi bạn lồng các cuộc gọi đến setTimeout/ setIntervalsâu hơn năm cấp; nếu bạn không có, tối thiểu là 0 ms (những gì thiếu máy thời gian). Các tài liệu của Mozilla mở rộng để bao gồm nhiều lần, không chỉ các trường hợp lồng nhau (vì vậy setIntervalvới khoảng 0 sẽ được lập lại ngay lập tức một vài lần, sau đó trì hoãn lâu hơn sau đó), nhưng sử dụng đơn giản setTimeoutvới lồng tối thiểu được phép xếp hàng ngay lập tức.
ShadowRanger

8

Vì nó đang được thông qua trong một khoảng thời gian 0, tôi cho rằng nó là để loại bỏ mã được truyền đến setTimeouttừ luồng thực thi. Vì vậy, nếu đó là một chức năng có thể mất một thời gian, nó sẽ không ngăn mã tiếp theo thực thi.


FYI: câu trả lời này đã được hợp nhất tại đây từ stackoverflow.com/questions/4574940/ cấp
Shog9

7

Cả hai câu trả lời hàng đầu này đều sai. Kiểm tra mô tả MDN trên mô hình đồng thời và vòng lặp sự kiện và nó sẽ trở nên rõ ràng những gì đang diễn ra (tài nguyên MDN đó là một viên ngọc thực sự). Và chỉ cần sử dụng setTimeout có thể thêm các vấn đề không mong muốn trong mã của bạn ngoài việc "giải quyết" vấn đề nhỏ này.

Điều thực sự xảy ra ở đây không phải là "trình duyệt có thể chưa hoàn toàn sẵn sàng vì đồng thời" hay một cái gì đó dựa trên "mỗi dòng là một sự kiện được thêm vào phía sau hàng đợi".

Các jsfiddle cung cấp bởi DVK thực sự minh họa một vấn đề, nhưng lời giải thích của mình cho nó là không đúng.

Điều đang xảy ra trong mã của anh ấy là lần đầu tiên anh ấy gắn một trình xử lý clicksự kiện vào sự kiện trên #donút.

Sau đó, khi bạn thực sự nhấp vào nút, a messageđược tạo tham chiếu chức năng xử lý sự kiện, được thêm vào message queue. Khi event loopđạt đến thông báo này, nó tạo ra một framengăn xếp, với chức năng gọi hàm xử lý sự kiện nhấp trong jsfiddle.

Và đây là nơi nó trở nên thú vị. Chúng ta đã quá quen với việc nghĩ rằng Javascript không đồng bộ đến mức chúng ta dễ bỏ qua thực tế nhỏ bé này: Bất kỳ khung nào cũng phải được thực hiện đầy đủ, trước khi khung tiếp theo có thể được thực thi . Không đồng thời, mọi người.

Điều đó có nghĩa là gì? Nó có nghĩa là bất cứ khi nào một chức năng được gọi từ hàng đợi tin nhắn, nó sẽ chặn hàng đợi cho đến khi ngăn xếp mà nó tạo ra đã bị xóa. Hoặc, nói một cách tổng quát hơn, nó chặn cho đến khi hàm trả về. Và nó chặn tất cả mọi thứ , bao gồm các hoạt động kết xuất DOM, cuộn và không có gì. Nếu bạn muốn xác nhận, chỉ cần cố gắng tăng thời lượng của hoạt động chạy dài trong fiddle (ví dụ: chạy vòng ngoài 10 lần nữa) và bạn sẽ nhận thấy rằng trong khi nó chạy, bạn không thể cuộn trang. Nếu nó chạy đủ lâu, trình duyệt của bạn sẽ hỏi bạn có muốn giết quá trình không, vì nó làm cho trang không phản hồi. Khung đang được thực thi và vòng lặp sự kiện và hàng đợi tin nhắn bị kẹt cho đến khi nó kết thúc.

Vậy tại sao tác dụng phụ này của văn bản không cập nhật? Bởi vì trong khi bạn đã thay đổi giá trị của phần tử trong DOM - bạn có thể giá trị của phần tử console.log()ngay lập tức sau khi thay đổi và thấy rằng nó đã bị thay đổi (điều này cho thấy lý do tại sao giải thích của DVK không chính xác) - trình duyệt đang chờ ngăn xếp hết ( onhàm xử lý trả về) và do đó thông báo kết thúc, để cuối cùng nó có thể đi xung quanh để thực thi thông báo đã được thêm bởi bộ thực thi như là một phản ứng đối với hoạt động đột biến của chúng tôi và để phản ánh sự đột biến đó trong giao diện người dùng .

Điều này là do chúng tôi thực sự đang chờ mã để chạy xong. Chúng tôi đã không nói "ai đó lấy kết quả này và sau đó gọi hàm này với kết quả, cảm ơn, và bây giờ tôi đã hoàn thành nên imma trở lại, làm bất cứ điều gì bây giờ", như chúng tôi thường làm với Javascript không đồng bộ dựa trên sự kiện. Chúng tôi nhập một hàm xử lý sự kiện nhấp chuột, chúng tôi cập nhật một phần tử DOM, chúng tôi gọi một hàm khác, hàm kia hoạt động trong một thời gian dài và sau đó trả về, sau đó chúng tôi cập nhật cùng một phần tử DOM và sau đó chúng tôi trở về từ hàm ban đầu, làm trống hiệu quả ngăn xếp. Và sau đó trình duyệt có thể nhận được tin nhắn tiếp theo trong hàng đợi, rất có thể là tin nhắn do chúng tôi tạo ra bằng cách kích hoạt một số sự kiện loại "on-DOM-mut" nội bộ.

Giao diện người dùng trình duyệt không thể (hoặc chọn không) cập nhật giao diện người dùng cho đến khi khung hiện đang thực thi đã hoàn thành (chức năng đã trở lại). Cá nhân, tôi nghĩ rằng điều này là do thiết kế hơn là hạn chế.

Tại sao setTimeoutđiều đó làm việc sau đó? Nó làm như vậy, bởi vì nó loại bỏ hiệu quả cuộc gọi đến chức năng chạy dài khỏi khung của chính nó, lên lịch để nó được thực hiện sau trong windowngữ cảnh, để nó có thể trả về ngay lập tức và cho phép hàng đợi tin nhắn xử lý các tin nhắn khác. Và ý tưởng là thông báo "cập nhật" UI đã được chúng tôi kích hoạt trong Javascript khi thay đổi văn bản trong DOM hiện trước tin nhắn được xếp hàng cho chức năng chạy dài, để cập nhật UI xảy ra trước khi chúng tôi chặn trong một khoảng thời gian dài.

Lưu ý rằng a) Hàm chạy dài vẫn chặn mọi thứ khi nó chạy và b) bạn không được đảm bảo rằng bản cập nhật UI thực sự đi trước nó trong hàng đợi tin nhắn. Trên trình duyệt Chrome tháng 6 năm 2018 của tôi, một giá trị 0không "khắc phục" sự cố mà fiddle thể hiện - 10 không. Tôi thực sự hơi bị cản trở bởi điều này, bởi vì có vẻ hợp lý với tôi rằng thông báo cập nhật giao diện người dùng nên được xếp hàng trước nó, vì trình kích hoạt của nó được thực thi trước khi lên lịch cho chức năng chạy dài để chạy "sau". Nhưng có lẽ có một số tối ưu hóa trong động cơ V8 có thể can thiệp, hoặc có thể sự hiểu biết của tôi chỉ là thiếu.

Được rồi, vậy vấn đề với việc sử dụng là setTimeoutgì, và giải pháp nào tốt hơn cho trường hợp cụ thể này?

Trước hết, vấn đề với việc sử dụng setTimeouttrên bất kỳ trình xử lý sự kiện nào như thế này, để cố gắng làm giảm bớt một vấn đề khác, có xu hướng gây rối với mã khác. Đây là một ví dụ thực tế từ công việc của tôi:

Một đồng nghiệp, theo cách hiểu sai về vòng lặp sự kiện, đã cố gắng "xâu chuỗi" Javascript bằng cách sử dụng một số mã kết xuất mẫu setTimeout 0để kết xuất. Anh ta không còn ở đây để hỏi nữa, nhưng tôi có thể đoán rằng có lẽ anh ta đã chèn bộ hẹn giờ để đánh giá tốc độ kết xuất (sẽ là sự trở lại trực tiếp của các chức năng) và thấy rằng việc sử dụng phương pháp này sẽ giúp đáp ứng nhanh chóng từ chức năng đó.

Vấn đề đầu tiên là rõ ràng; bạn không thể tạo luồng javascript, vì vậy bạn không giành được gì ở đây trong khi bạn thêm obfuscation. Thứ hai, bây giờ bạn đã tách ra một cách hiệu quả việc hiển thị một mẫu từ chồng các trình lắng nghe sự kiện có thể có thể mong đợi rằng chính mẫu đó đã được hiển thị, trong khi nó rất có thể không được. Hành vi thực tế của chức năng đó bây giờ không mang tính quyết định, như là - vô tình là như vậy - bất kỳ chức năng nào sẽ chạy nó, hoặc phụ thuộc vào nó. Bạn có thể đưa ra những phỏng đoán có giáo dục, nhưng bạn không thể viết mã đúng cho hành vi của nó.

"Sửa" khi viết một trình xử lý sự kiện mới phụ thuộc vào logic của nó là cũng sử dụng setTimeout 0. Nhưng, đó không phải là một sửa chữa, thật khó hiểu và không có gì vui khi gỡ lỗi các lỗi gây ra bởi mã như thế này. Đôi khi không có vấn đề gì, đôi khi nó liên tục thất bại, và một lần nữa, đôi khi nó hoạt động và phá vỡ một cách rời rạc, tùy thuộc vào hiệu suất hiện tại của nền tảng và bất cứ điều gì khác xảy ra vào thời điểm đó. Đây là lý do tại sao cá nhân tôi khuyên bạn không nên sử dụng bản hack này (đây bản hack và tất cả chúng ta nên biết rằng đó là), trừ khi bạn thực sự biết bạn đang làm gì và hậu quả là gì.

Nhưng thay vào đó chúng ta có thể làm gì? Chà, như bài viết MDN được tham chiếu cho thấy, hãy chia công việc thành nhiều tin nhắn (nếu bạn có thể) để các tin nhắn khác được xếp hàng có thể được xen kẽ với công việc của bạn và được thực thi trong khi nó chạy, hoặc sử dụng một nhân viên web, có thể chạy song song với trang của bạn và trả về kết quả khi thực hiện với các tính toán của nó.

Ồ, và nếu bạn đang nghĩ, "Chà, tôi không thể đặt một cuộc gọi lại trong chức năng chạy dài để làm cho nó không đồng bộ?," Thì không. Cuộc gọi lại không làm cho nó không đồng bộ, nó sẽ vẫn phải chạy mã chạy dài trước khi gọi lại cuộc gọi lại của bạn một cách rõ ràng.


3

Một điều khác làm điều này là đẩy lời gọi hàm xuống dưới cùng của ngăn xếp, ngăn chặn tràn ngăn xếp nếu bạn đang gọi đệ quy một hàm. Điều này có tác dụng của một whilevòng lặp nhưng cho phép công cụ JavaScript kích hoạt các bộ định thời không đồng bộ khác.


Downvote cho điều này push the function invocation to the bottom of the stack. Bạn stackđang nói về cái gì là tối nghĩa. Quan trọng nhất là nơi chính xác setTimeoutthêm func này, kết thúc chu kỳ vòng lặp này hoặc bắt đầu chu kỳ vòng lặp tiếp theo.
Màu xanh lá cây

1

Bằng cách gọi setTimeout, bạn cho thời gian trang để phản ứng với bất cứ điều gì người dùng đang làm. Điều này đặc biệt hữu ích cho các chức năng chạy trong khi tải trang.


1

Một số trường hợp khác trong đó setTimeout hữu ích:

Bạn muốn phá vỡ một vòng lặp dài hoặc tính toán thành các thành phần nhỏ hơn để trình duyệt không xuất hiện để 'đóng băng' hoặc nói "Tập lệnh trên trang đang bận".

Bạn muốn tắt nút gửi biểu mẫu khi nhấp, nhưng nếu bạn tắt nút trong trình xử lý onClick, biểu mẫu sẽ không được gửi. setTimeout với thời gian bằng 0 thực hiện thủ thuật, cho phép sự kiện kết thúc, biểu mẫu bắt đầu gửi, sau đó nút của bạn có thể bị vô hiệu hóa.


2
Vô hiệu hóa sẽ được thực hiện tốt hơn trong sự kiện onsubmit; nó sẽ nhanh hơn và được đảm bảo được gọi trước khi biểu mẫu được gửi kỹ thuật vì bạn có thể dừng việc gửi.
Kris

Rất đúng. Tôi cho rằng việc vô hiệu hóa onclick dễ dàng hơn cho việc tạo mẫu vì bạn chỉ cần gõ onclick = "this.disables = true" trong nút trong khi vô hiệu hóa khi gửi yêu cầu công việc hơi nhiều.
fabspro

1

Các câu trả lời về các vòng lặp thực thi và hiển thị DOM trước khi một số mã khác hoàn thành là chính xác. Không có thời gian chờ thứ hai trong JavaScript giúp làm cho mã giả đa luồng, mặc dù không phải vậy.

Tôi muốn thêm rằng giá trị TỐT NHẤT cho thời gian chờ 0 giây của trình duyệt chéo / nền tảng chéo trong JavaScript thực sự là khoảng 20 mili giây thay vì 0 (không), vì nhiều trình duyệt di động không thể đăng ký thời gian chờ nhỏ hơn 20 mili giây do giới hạn đồng hồ trên chip AMD.

Ngoài ra, ngay bây giờ, các quy trình chạy dài không liên quan đến thao tác DOM nên được gửi đến Công nhân web, vì chúng cung cấp thực thi JavaScript đa luồng thực sự.


2
Tôi hơi nghi ngờ về câu trả lời của bạn, nhưng đã ủng hộ nó vì nó buộc tôi phải thực hiện một số nghiên cứu bổ sung về các tiêu chuẩn trình duyệt. Khi nghiên cứu các tiêu chuẩn, tôi đi đến nơi tôi luôn đến, MDN: developer.mozilla.org/en-US/docs/Web/API/window.setTimeout Thông số HTML5 cho biết 4ms. Nó không nói bất cứ điều gì về giới hạn đồng hồ trên chip di động. Có một thời gian khó khăn để tìm kiếm một nguồn thông tin để sao lưu các báo cáo của bạn. Đã tìm ra Ngôn ngữ phi tiêu của Google đã loại bỏ setTimeout hoàn toàn có lợi cho một đối tượng Timer.
John Zabroski

8
(...) bởi vì nhiều trình duyệt di động không thể đăng ký thời gian chờ nhỏ hơn 20 mili giây do giới hạn đồng hồ (...) Mọi nền tảng đều có giới hạn thời gian do đồng hồ của nó và không có nền tảng nào có khả năng thực hiện điều tiếp theo chính xác 0ms sau hiện tại. Hết thời gian 0ms yêu cầu thực thi một chức năng càng sớm càng tốt và giới hạn thời gian của nền tảng cụ thể không thay đổi ý nghĩa của việc này theo bất kỳ cách nào.
Piotr Dobrogost

1

setTimout on 0 cũng rất hữu ích trong mô hình thiết lập một lời hứa bị hoãn, mà bạn muốn quay lại ngay:

myObject.prototype.myMethodDeferred = function() {
    var deferredObject = $.Deferred();
    var that = this;  // Because setTimeout won't work right with this
    setTimeout(function() { 
        return myMethodActualWork.call(that, deferredObject);
    }, 0);
    return deferredObject.promise();
}

1

Vấn đề là bạn đã cố gắng thực hiện một thao tác Javascript trên một phần tử không tồn tại. Phần tử vẫn chưa được tải và setTimeout()dành nhiều thời gian hơn cho phần tử tải theo các cách sau:

  1. setTimeout()gây sự kiện để được ansynchronous do đó được thực hiện sau khi tất cả các mã đồng bộ, tạo cho phần tử của bạn thêm thời gian để tải. Các cuộc gọi lại không đồng bộ như cuộc gọi lại setTimeout()được đặt trong hàng đợi sự kiện và được đặt vào ngăn xếp bởi vòng lặp sự kiện sau khi ngăn xếp mã đồng bộ trống.
  2. Giá trị 0 cho ms làm đối số thứ hai trong hàm setTimeout()thường cao hơn một chút (4-10ms tùy theo trình duyệt). Thời gian cao hơn một chút cần thiết để thực hiện các setTimeout()cuộc gọi lại được gây ra bởi số lượng 'tick' (trong đó một tick đang đẩy một cuộc gọi lại trên ngăn xếp nếu ngăn xếp trống) của vòng lặp sự kiện. Vì lý do hiệu suất và tuổi thọ pin, số lượng bọ trong vòng sự kiện bị giới hạn ở một mức nhất định dưới 1000 lần mỗi giây.

-1

Javascript là một ứng dụng luồng đơn để không cho phép chạy đồng thời chức năng để đạt được các vòng lặp sự kiện này. Vì vậy, chính xác những gì setTimeout (fn, 0) thực hiện mà nhiệm vụ của nó được thực hiện trong nhiệm vụ được thực hiện khi ngăn xếp cuộc gọi của bạn trống. Tôi biết lời giải thích này khá nhàm chán, vì vậy tôi khuyên bạn nên xem qua video này, điều này sẽ giúp bạn làm thế nào mọi thứ hoạt động dưới mui xe trong trình duyệt. Xem video này: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.