Обидва ці два найвищі відповіді - неправильні. Ознайомтеся з описом MDN на моделі одночасності та циклом подій , і вам повинно стати зрозумілим, що відбувається (що ресурс MDN - це справжня дорогоцінний камінь). А просто використання setTimeout
може додавати до коду несподівані проблеми на додаток до «вирішення» цієї маленької проблеми.
Тут насправді відбувається не те, що "браузер може бути не зовсім готовим, оскільки одночасність", або щось на основі "кожного рядка - це подія, яка додається в задній частині черги".
Jsfiddle забезпечується ДВК дійсно ілюструє проблему, але його пояснення цьому не є правильним.
Що відбувається в його коді - це те, що він спочатку прикріплює обробник події до click
події на #do
кнопці.
Потім, коли ви фактично натискаєте кнопку, message
створюється a із посиланням на функцію обробника подій, яка додається до message queue
. Коли event loop
це повідомлення досягає, воно створює frame
на стеці з викликом функції до обробника подій клацання у jsfiddle.
І тут стає цікаво. Ми настільки звикли думати Javascript як асинхронний, що ми схильні нехтувати цим крихітним фактом: будь-який кадр повинен бути виконаний у повному обсязі, перш ніж наступний кадр може бути виконаний . Ніякої сукупності, люди.
Що це означає? Це означає, що кожного разу, коли функція викликається з черги повідомлень, вона блокує чергу, поки стек, який вона створює, не буде спустошений. Або, у більш загальних рисах, він блокується, поки функція не повернеться. І це блокує все , включаючи операції з рендерінгу DOM, прокрутку тощо. Якщо ви хочете підтвердження, просто спробуйте збільшити тривалість тривалої операції у скрипці (наприклад, запустіть зовнішню петлю ще 10 разів), і ви помітите, що під час її запуску ви не можете прокручувати сторінку. Якщо він працює досить довго, ваш браузер запитає вас, чи хочете ви вбити процес, оскільки це робить сторінку невідповідною. Кадр виконується, і цикл подій та черга повідомлень застрягають, поки він не закінчиться.
То чому цей побічний ефект тексту не оновлюється? Тому що в той час як ви вже змінили значення елемента в DOM - ви можете console.log()
його значення відразу після його зміни і бачити , що він був змінений (який показує , чому пояснення ДВК неправильно) - браузер чекає стек виснажувати ( on
функція обробника для повернення), і, таким чином, повідомлення закінчується, щоб в кінцевому підсумку можна було обійтися виконанням повідомлення, яке було додано під час виконання, як реакція на нашу операцію мутації, і щоб відобразити цю мутацію в інтерфейсі користувача .
Це тому, що ми насправді чекаємо, коли код закінчить працювати. Ми не говорили, що "хтось це отримує, а потім викликає цю функцію з результатами, дякую, і тепер я зробив так, що повертаюсь до Imma, роби все, що зараз", як це зазвичай робимо з нашим подійним асинхронним Javascript. Ми вводимо функцію обробника подій клацання, ми оновлюємо елемент DOM, ми викликаємо іншу функцію, інша функція працює тривалий час, а потім повертається, ми оновлюємо той самий елемент DOM, а потім повертаємося з початкової функції, ефективно спорожняючи стек. І тоді браузер може перейти до наступного повідомлення в черзі, яке, можливо, може бути повідомленням, яке генерується нами, викликаючи якесь внутрішнє подія типу "DOM-мутація".
Користувацький інтерфейс браузера не може (або не бажає) оновити інтерфейс користувача, поки не завершиться поточний кадр, що виконується (функція повернулася). Особисто я вважаю, що це скоріше дизайн, ніж обмеження.
Чому setTimeout
річ працює тоді? Це робиться так, тому що він ефективно видаляє виклик тривалої функції з власного кадру, плануючи її виконання пізніше в window
контексті, щоб він сам негайно повертався і дозволяв черзі повідомлень обробляти інші повідомлення. Ідея полягає в тому, що повідомлення "оновлення" інтерфейсу, яке було запущено нами в Javascript при зміні тексту в DOM, тепер випереджає повідомлення, що стоїть на черзі для тривалої функції, так що оновлення інтерфейсу відбувається, перш ніж ми заблокуємо довго.
Зауважте, що а) Функція, яка триває, все ще блокує все, коли вона працює, і б) вам не гарантується, що оновлення інтерфейсу дійсно випереджає її в черзі повідомлень. У моєму веб-переглядачі Chrome у червні 2018 року значення 0
не "вирішує" проблему, яку демонструє скрипка - 10. Я насправді трохи задушений цим, тому що мені здається логічним, що повідомлення про оновлення інтерфейсу повинно бути в черзі перед ним, оскільки його тригер виконується перед плануванням тривалої функції, яку потрібно запустити "пізніше". Але, можливо, в двигуні V8 є якісь оптимізації, які можуть заважати, а може, я просто не розумію.
Гаразд, так у чому проблема використання setTimeout
та яке краще рішення для даного конкретного випадку?
По-перше, проблема з використанням setTimeout
будь-якого обробника подій, як це, щоб спробувати полегшити іншу проблему, схильна до псування з іншим кодом. Ось приклад із моєї роботи:
Колега, неправильно зрозумівши цикл подій, спробував "потік" Javascript, використовуючи якийсь код візуалізації шаблону setTimeout 0
для його візуалізації. Він більше не тут, щоб запитувати, але я можу припустити, що, можливо, він вставив таймери для вимірювання швидкості візуалізації (що було б зворотною безпосередністю функцій) і виявив, що використання цього підходу спричинить блискавично швидкі відповіді від цієї функції.
Перша проблема очевидна; ви не можете надати тему javascript, тому ви нічого не виграєте, додаючи притуплення. По-друге, тепер ви фактично від'єднали візуалізацію шаблону зі стека можливих слухачів подій, які, можливо, очікують, що цей шаблон буде виведений, хоча це, можливо, ще не було. Фактична поведінка цієї функції тепер була недетермінованою, як це було - несвідомо - будь-якою функцією, яка би виконувала її чи залежала від неї. Ви можете робити здогадки, але ви не можете правильно кодувати його поведінку.
"Виправлення" при написанні нового обробника подій, яке залежало від його логіки, також слід було використовувати setTimeout 0
. Але це не виправлення, це важко зрозуміти, і не весело налагоджувати помилки, викликані таким кодом. Іноді проблем ніколи не виникає, в інших випадках він послідовно виходить з ладу, а потім знову, іноді він працює і порушується епізодично, залежно від поточної продуктивності платформи та всього іншого, що відбувається в той час. Тому особисто я б порадив не використовувати цей хак (він є хак, і ми всі повинні знати , що це таке), якщо ви дійсно не знаєте , що ви робите , і які його наслідки.
Але що можемо ми робити замість цього? Ну, як підказує посилання на статтю MDN, або розділіть роботу на кілька повідомлень (якщо ви можете), щоб інші повідомлення, які стоять у черзі, могли бути переплетені з вашою роботою та виконані під час її запуску, або використовувати веб-працівника, який може працювати у тандемі зі своєю сторінкою та повертайте результати, коли завершите її обчислення.
О, і якщо ви думаєте: "Ну хіба я не міг би просто поставити зворотній дзвінок у функцію, що триває, щоб зробити його асинхронним?", То ні. Зворотний виклик не робить його асинхронним, все одно доведеться запустити тривалий код, перш ніж явно викликати зворотний дзвінок.