Оба из этих двух самых популярных ответов неверны. Ознакомьтесь с описанием MDN для модели параллелизма и цикла обработки событий , и должно стать понятно, что происходит (этот ресурс MDN является настоящим сокровищем). И простое использование setTimeout
может добавить неожиданные проблемы в ваш код в дополнение к «решению» этой маленькой проблемы.
То, что на самом деле происходит здесь, вовсе не означает, что "браузер может быть еще не совсем готов, потому что параллелизм" или что-то на основе "каждая строка - это событие, которое добавляется в конец очереди" .
jsfiddle , предоставленный DVK, действительно иллюстрирует проблему, но его объяснение не является правильным.
Что происходит в его коде, так это то, что он сначала присоединяет обработчик события к событию click
на кнопке #do
.
Затем, когда вы фактически нажимаете кнопку, создается message
, ссылающийся на функцию обработчика событий, которая добавляется к message queue
. Когда event loop
достигает этого сообщения, оно создает frame
в стеке с вызовом функции для обработчика события click в jsfiddle.
И вот тут-то и становится интересно. Мы настолько привыкли считать Javascript асинхронным, что склонны упускать из виду этот крошечный факт: Любой кадр должен быть выполнен полностью, прежде чем следующий кадр может быть выполнен . Нет параллелизма, люди.
Что это значит? Это означает, что всякий раз, когда функция вызывается из очереди сообщений, она блокирует очередь до тех пор, пока сгенерированный ею стек не будет очищен. Или, в более общих чертах, он блокируется, пока функция не вернется. И он блокирует все , включая операции рендеринга DOM, прокрутки и еще много чего. Если вам нужно подтверждение, просто попробуйте увеличить продолжительность длительной операции в скрипте (например, запустить внешний цикл еще 10 раз), и вы заметите, что во время его работы вы не можете прокручивать страницу. Если он работает достаточно долго, ваш браузер спросит вас, хотите ли вы завершить процесс, потому что он делает страницу не отвечающей. Кадр выполняется, а цикл обработки событий и очередь сообщений застревают до его завершения.
Так почему же этот побочный эффект текста не обновляется? Потому что, хотя вы изменили значение элемента в DOM - вы можете console.log()
его значение сразу после его изменения и увидеть, что было изменено (что показывает, почему объяснение DVK неправильно) - браузер ожидает исчерпания стека (функция-обработчик on
возвращает) и, таким образом, сообщения завершается, чтобы в конечном итоге можно было выполнить сообщение, добавленное средой выполнения как реакция на нашу операцию мутации, и чтобы отразить эту мутацию в пользовательском интерфейсе.
Это потому, что мы на самом деле ожидаем завершения кода. Мы не сказали «кто-то получит это, а затем вызовет эту функцию с результатами, спасибо, и теперь я закончил с возвращением imma, делайте что угодно сейчас», как мы обычно делаем с нашим основанным на событиях асинхронным Javascript. Мы вводим функцию обработчика события щелчка, мы обновляем элемент DOM, вызываем другую функцию, другая функция работает долгое время, а затем возвращается, затем мы обновляем тот же элемент DOM, и , затем мы возвращаем из начальная функция, эффективно очищающая стек. И затем браузер может перейти к следующему сообщению в очереди, которое вполне может быть сообщением, сгенерированным нами путем запуска некоторого внутреннего события типа «on-DOM-mutation».
Пользовательский интерфейс браузера не может (или не хочет) обновлять пользовательский интерфейс до тех пор, пока не завершится текущий исполняемый кадр (функция не вернулась). Лично я думаю, что это скорее дизайн, чем ограничение.
Почему тогда работает setTimeout
? Это происходит потому, что он эффективно удаляет вызов длительно выполняющейся функции из своего собственного фрейма, планируя ее последующее выполнение в контексте window
, чтобы он сам мог немедленно возвратить и разрешить очередь сообщений для обработки других сообщений. И идея заключается в том, что сообщение «при обновлении» пользовательского интерфейса, которое было сгенерировано нами в Javascript при изменении текста в DOM, теперь опережает сообщение, помещенное в очередь для долго выполняющейся функции, поэтому обновление пользовательского интерфейса происходит до того, как мы заблокируем в течение длительного времени.
Обратите внимание, что a) долгосрочная функция по-прежнему блокирует все, когда она запускается, и b) вам не гарантируется, что обновление пользовательского интерфейса действительно опережает его в очереди сообщений. В моем браузере Chrome в июне 2018 года значение 0
не «решает» проблему, которую демонстрирует скрипка - 10 делает. Я на самом деле немного задушен этим, потому что мне кажется логичным, что сообщение об обновлении пользовательского интерфейса должно быть поставлено в очередь перед ним, так как его триггер выполняется перед планированием долгосрочной функции, которая будет запущена «позже». Но, возможно, в движке V8 есть некоторые оптимизации, которые могут помешать, или, возможно, мне просто не хватает понимания.
Хорошо, так в чем же проблема с использованием setTimeout
, и что является лучшим решением для этого конкретного случая?
Во-первых, проблема с использованием setTimeout
в любом обработчике событий, подобном этому, чтобы попытаться устранить другую проблему, склонна связываться с другим кодом. Вот реальный пример из моей работы:
Коллега, в неправильном понимании цикла обработки событий, попытался «обработать» Javascript, используя некоторый код рендеринга шаблонов, использующий setTimeout 0
для его рендеринга. Его больше нет здесь, чтобы спрашивать, но я могу предположить, что, возможно, он вставил таймеры для измерения скорости рендеринга (что было бы возвращением непосредственности функций) и обнаружил, что использование этого подхода приведет к невероятно быстрым ответам от этой функции.
Первая проблема очевидна; вы не можете использовать javascript, поэтому вы ничего не выиграете, пока добавляете запутывание. Во-вторых, вы теперь эффективно отсоединили рендеринг шаблона от стека возможных прослушивателей событий, которые могут ожидать, что этот шаблон был отрендерен, хотя вполне возможно, что это не так. Реальное поведение этой функции было теперь недетерминированным, как и, по незнанию, любая функция, которая будет запускать ее или зависеть от нее. Вы можете делать обоснованные предположения, но не можете правильно кодировать его поведение.
"Исправление" при написании нового обработчика событий, которое зависело от его логики, заключалось в также использовании setTimeout 0
. Но это не исправление, это трудно понять, и неинтересно отлаживать ошибки, вызванные таким кодом. Иногда проблем никогда не бывает, иногда постоянно происходит сбой, а затем, опять же, иногда это работает и прерывается время от времени, в зависимости от текущей производительности платформы и того, что еще происходит в данный момент. Вот почему я лично советую не использовать этот хак (это - это хак, и мы все должны знать, что это так), если вы действительно не знаете, что делаете и каковы последствия.
Но что можно сделать вместо этого? Что ж, как указано в упомянутой статье MDN, либо разделите работу на несколько сообщений (если можете), чтобы другие сообщения, находящиеся в очереди, могли чередоваться с вашей работой и выполнялись во время ее выполнения, либо используйте веб-работника, который может запустить в тандеме с вашей страницей и возвращать результаты, когда закончите ее вычисления.
О, и если вы думаете: «Ну, я не могу просто добавить обратный вызов в долговременную функцию, чтобы сделать ее асинхронной?», Тогда нет. Обратный вызов не делает его асинхронным, ему все равно придется выполнить долго работающий код, прежде чем явно вызвать ваш обратный вызов.