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