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ý click
sự kiện vào sự kiện trên #do
nú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 frame
ngă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 ( on
hà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 window
ngữ 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ị 0
khô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à setTimeout
gì, 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 setTimeout
trê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 là 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.