Javascript, которого вы не знали: забавный setTimeout

внешний интерфейс JavaScript

Просматривая сегодня расширенное использование JavaScript, я обнаружил интересную проблему.Без лишних слов, давайте начнем с кода:

    for(var j=0;j<10;j++){
      setTimeout(function(){console.log(j)},5000)
    }

Когда вы видите эти три строчки кода, вы можете нетерпеливо сказать: «Хочешь еще раз поговорить о замыканиях?» Вы собираетесь рвать? Не волнуйтесь, давайте сначала подумаем, каков результат выполнения этого кода в браузере?

A: Напечатать от 0 до 9 последовательно?

Б: Я уже видел этот вопрос раньше, напечатайте десять десятков!

Какой ответ правильный? Продолжим с картинкой выше:

Результат выполнения показывает, что браузер печатает десять десятков (по причине обработки изображения, между нажатием Enter и печатью на самом деле около 5 секунд), кажется, что выиграл B. Но если вы будете достаточно внимательны, вы обнаружите несколько проблем:

  1. Почему цикл печатает десять десятков вместо цифр от 0 до 9?
  2. Судя по результатам, setTimeout выполняется только после того, как цикл for выполнен и выскочил (поэтому j равно 10).Почему бы не выполнить setTimeout один раз за итерацию?

Если вы можете ответить на три вопроса выше, поздравляю, вы начали осваивать глубокие знания JavaScript, если нет, то смотрите вниз!

Почему он печатает десять десятков в цикле

Многие привыкли отвечать на этот вопрос результатом выполнения во втором вопросе: «После того, как цикл for выполнен и выскочил, выполняется setTimeout, поэтому печатается десять десятков». О таком ответе можно только сказать, что он способен справиться как с собой, так и с другими. На самом деле, чтобы ответить на первый вопрос, нужно сначала ответить на второй вопрос.

Почему setTimeout не выполняется один раз за итерацию

Как мы все знаем, в JavaScript не было блочной области видимости до появления ES6, а это значит, что переменная j, определенная с помощью var в цикле for, на самом деле является глобальной, то есть к ней можно получить доступ в глобальной области видимости, в данном случае , на самом деле есть только один j во всей глобальной области видимости, и i обновляет этот j каждый раз, когда цикл for.

Итак, теперь ключевой вопрос заключается в том, почему весь цикл for будет выполняться до setTimeout, а не выполняться один раз за итерацию, как мы обычно понимаем.

Это включает в себя основную функцию JavaScript: один поток.

Первоначально JavaScript был разработан для использования браузерами для взаимодействия с пользователями и управления DOM. Это определяет, что он должен быть однопоточным.Представьте, что коллеги по JavaScript имеют два потока, один поток добавляет содержимое в узел DOM, а другой поток удаляет узел, и браузер будет сбит с толку. Таким образом, чтобы избежать сложности, JavaScript с самого начала был однопоточным, что было основной особенностью языка и не изменится в будущем.

Однопоточный означает, что все задачи должны быть поставлены в очередь, а следующая задача будет выполняться после завершения предыдущей задачи. Если первая задача занимает много времени, вторая задача должна ждать вечно.

Чтобы оптимизировать производительность одного потока, JavaScript делит задачи на два типа: синхронные и асинхронные. Синхронные задачи относятся к задачам, поставленным в очередь для выполнения в основном потоке, и следующая задача может быть выполнена только после выполнения предыдущей задачи; асинхронные задачи относятся к задачам, которые не входят в основной поток, а входят в «очередь задач» ( очередь задач) Задача, асинхронная задача войдет в очередь выполнения для выполнения только после того, как будет выполнена синхронная задача в основном потоке. Пока основной поток пуст, он будет читать «очередь задач», как работает JavaScript. Этот процесс будет повторяться.

А setTimeout определяется JavaScript как асинхронная задача. На каждой итерации цикла for функция обратного вызова в setTimeout добавляется в очередь задач для выполнения. То есть, только когда цикл for в задаче синхронизации будет полностью завершен, основной поток отправится в очередь задач, чтобы найти десять функций обратного вызова setTimeout (десять итераций), которые не были выполнены, и выполнить их последовательно (первый в очереди). , первый выход). В это время i стало 10 после окончания цикла, поэтому в это время основной поток выполняет десять одинаковых функций обратного вызова, которые печатают i, то есть печатают десять десятков. До сих пор на первый и второй вопросы были даны точные ответы Код в начале статьи фактически эквивалентен следующему коду:

    for(var i=0;i<10;i++){}
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)
    setTimeout(console.log(i),5000)

Небольшой setTimeout включает в себя множество проблем глубокого уровня в JavaScript.Хотя он кратко изложен в статье всего из нескольких сотен слов, я ознакомился с большим количеством информации и провел много экспериментов в процессе написания.

Напоследок дам небольшой, но все же беспокоящий меня вопрос, надеюсь, заинтересованные друзья смогут со мной поучиться:

    setTimeout(function(){while(true){}},6000);
    setTimeout(function(){console.log(1)},10000);
    setTimeout(function(){console.log(2)},5000);

Каков порядок выполнения приведенного выше кода? Время setTimeout, выполняется ли оно сразу после того, как время вставлено в стек выполнения, или оно вставлено в стек выполнения для выполнения времени выполнения?

С нетерпением жду ваших сообщений.