Цель этой статьи — убедиться, что вы полностью понимаете механизм выполнения javascript. Если вы не поняли после прочтения этой статьи, вы можете побить меня.

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

  • javascript выполняется в том порядке, в котором появляются операторы

Читатели могут ударить меня, когда увидят это. Разве я не знаю, что js выполняется построчно? Тебе еще нужно сказать? Не волнуйтесь, потому что js выполняется построчно, поэтому мы думаем, что js выглядит так:

let a = '1';
console.log(a);
let b = '2';
console.log(b);

Однако на самом деле js такой

setTimeout(function(){
    console.log('The timer starts')
});
new Promise(function(resolve){
    console.log('Execute the for loop now');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('Execute the then function')
});
console.log('Code execution ends');

Согласно концепции, что js выполняется в том порядке, в котором появляются операторы, я уверенно записываю вывод:

//“The timer starts”
//“Execute the for loop now”
//“Execute the then function”
//“Code execution ends”

Перейдите в Chrome, чтобы проверить, результат совершенно неверный, и я мгновенно ошеломлен. Как насчет выполнения построчно?

Нам действительно нужно досконально понять механизм выполнения javascript.

1. О JavaScript

javascript — это однопоточный язык, а Web-Worker был предложен в последней версии HTML5, но суть однопоточного javascript не изменилась. Поэтому все версии javascript «многопоточности» моделируются с помощью одного потока, а вся многопоточность javascript — это бумажный тигр!

2. Цикл событий javascript

Поскольку js является однопоточным, он похож на банк только с одним окном. Клиенты должны стоять в очереди, чтобы справиться с бизнесом один за другим. Точно так же задачи js также должны выполняться одна за другой. Если одна задача занимает слишком много времени, последняя задача также должна ждать. Итак, вопрос в том, хотим ли мы просматривать новости, но сверхчеткие картинки, содержащиеся в новостях, загружаются очень медленно, должна ли веб-страница зависнуть до полного отображения картинок? явно не должен!Итак, умные программисты делят задачи на две категории:

  • Синхронная задача
  • асинхронная задача

Когда мы открываем веб-сайт, процесс рендеринга веб-страницы представляет собой множество синхронных задач, таких как рендеринг скелета страницы и элементов страницы. Задачи, которые занимают много ресурсов и занимают много времени, например, загрузка изображений. и музыка, являются асинхронными задачами. Для этой части существуют строгие текстовые определения, но цель этой статьи состоит в том, чтобы полностью понять механизм выполнения с минимальными затратами на обучение, поэтому мы используем карту для иллюстрации:

Если содержание, которое должно быть выражено картой, выражено словами:

  1. Синхронные и асинхронные задачи попадают в разные «места» выполнения, синхронно входят в основной поток, асинхронно попадают в Таблицу событий и регистрируют функции.
  2. Когда указанное действие будет выполнено, таблица событий переместит эту функцию в очередь событий.
  3. После того, как задача в основном потоке опустеет, она отправится в Очередь событий, чтобы прочитать соответствующую функцию и войти в основной поток для выполнения.
  4. Вышеупомянутый процесс будет продолжать повторяться, что часто называют циклом событий.

Мы не можем не спросить, как узнать, что стек выполнения основного потока пуст? В движке js есть процесс мониторинга, который будет постоянно проверять, пуст ли стек выполнения основного потока. Как только он станет пустым, он перейдет в очередь событий, чтобы проверить, есть ли функции, ожидающие вызова.

Сказав так много текста, проще направить кусок кода:

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('sent successfully!');
    }
})
console.log('code execution end');

Выше приведен простой код запроса ajax:

  1. Ajax входит в таблицу событий и регистрирует успех функции обратного вызова.
  2. Выполнить console.log('конец выполнения кода
  3. Событие ajax завершено, и успешное выполнение функции обратного вызова попадает в очередь событий.
  4. Основной поток считывает успешную функцию обратного вызова из очереди событий и выполняет ее.

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

3.Любовь-ненависть setTimeout

Знаменитый setTimeout не нуждается в дополнительных словах. Первое впечатление о нем у всех такое, что асинхронное выполнение может быть отложено. Мы часто реализуем отложенное выполнение на 3 секунды так:

setTimeout(() => {
    console.log('delay 3 seconds');
},3000)

Постепенно setTimeout используется во все большем количестве мест, и также возникают проблемы. Иногда пишут, что задержка 3 секунды, а на самом деле выполняется за 5 или 6 секунд. Что происходит?

Давайте посмотрим на пример:

setTimeout(() => {
  task();
},3000)
console.log('execute console');

Согласно нашему выводу выше, setTimeout является асинхронным, и синхронная задача console.log должна выполняться первой, поэтому наш вывод таков:

//execute console
//task()

Проверьте, результат правильный! Затем мы модифицируем предыдущий код:

setTimeout(() => {
  task()
},3000)
sleep(10000000)

На первый взгляд это почти то же самое, но когда мы выполняем этот код в chrome, мы обнаруживаем, что время, необходимое консоли для выполнения task(), намного превышает 3 секунды. Задержка составляет три секунды, так почему теперь это занимает так много времени, а?

В настоящее время нам нужно заново понять определение setTimeout. Во-первых, давайте посмотрим, как выполняется приведенный выше код:

  1. task() входит в Event Table и регистрируется, и начинается отсчет времени.
  2. Выполните функцию сна, очень медленно, очень медленно, и время продолжится.
  3. 3 секунды истекли, тайм-аут события синхронизации завершен, задача () входит в очередь событий, но спящий режим слишком медленный, он еще не выполнен, поэтому мне нужно подождать.
  4. Sleep, наконец, выполняется, и задача () наконец входит в основной поток из очереди событий для выполнения.

После завершения вышеуказанного процесса мы знаем, что функция setTimeout добавляет задачу для выполнения (task() в этом примере) в очередь событий через указанное время, и поскольку это однопоточная задача, ее необходимо выполняются один за другим. Предыдущая задача занимает слишком много времени, поэтому вам остается только ждать, в результате чего реальное время задержки превышает 3 секунды.

Мы также часто сталкиваемся с таким кодом, как setTimeout(fn,0), что значит выполнить через 0 секунд? Можно ли его выполнить немедленно?

Ответ - нет. Значение setTimeout(fn, 0) заключается в том, чтобы указать, что задача выполняется в самое раннее доступное время простоя основного потока, а это означает, что нет необходимости ждать, сколько секунд, пока основной поток выполняет все задачи синхронизации в стеке Когда выполнение завершено, стек пуст и выполняется немедленно. например:

//code 1
console.log('execute here first');
setTimeout(() => {
  console.log('executed')
},0);
//code 2
console.log('execute here first');
setTimeout(() => {
     console.log('executed')
},3000);

Вывод кода 1:

// execute this first
// execute

Вывод кода 2:

// first execute here
// ... 3s later
// execute

Одна вещь, которую следует добавить о setTimeout, заключается в том, что даже если основной поток пуст, 0 миллисекунд фактически недостижимы. Согласно стандартам HTML, минимум составляет 4 миллисекунды. Заинтересованные друзья могут узнать об этом сами.

4. Ненависть-любовь setInterval

После разговора о setTimeout выше, конечно, вы не можете пропустить его брата-близнеца setInterval. Они похожи, за исключением того, что последний представляет собой выполнение цикла. Для порядка выполнения setInterval будет помещать зарегистрированную функцию в очередь событий каждый указанный момент времени. Если предыдущая задача занимает слишком много времени, ее также нужно подождать.

Единственное, что следует отметить, это то, что для setInterval(fn,ms) мы уже знаем, что fn выполняется не каждые мс секунд, но каждые мс секунд fn будет попадать в очередь событий. По истечении времени выполнения функции обратного вызова fn из setInterval превышает время задержки мс, то временной разрыв отсутствует вообще. Читателей просят внимательно смаковать это предложение.

5.Promise и process.nextTick(обратный вызов)

Мы уже изучили традиционные таймеры. Далее давайте рассмотрим Promise и process.nextTick(callback). Определение и функции Promise не будут повторяться в этой статье. Читатели, которые не понимают, могут узнать о Promise в MDN. И process.nextTick(callback) похож на версию setTimeout для node.js, которая вызывает callback-функцию обратного вызова в следующем цикле цикла событий.

Переходим к сути, помимо обобщенных синхронных задач и асинхронных задач у нас есть более уточненное определение задач:

  • макрозадача: включая общий код скрипта, setTimeout, setInterval
  • микрозадача: Promise, process.nextTick

Задачи разных типов попадут в соответствующую очередь событий, например, setTimeout и setInterval попадут в одну и ту же очередь событий.

Порядок цикла событий определяет порядок выполнения кода js. После ввода общего кода (макрозадачи) запустите первый цикл. Затем выполните все микрозадачи. Затем снова начните с макрозадачи, обнаружите, что одна из очередей задач была выполнена, а затем выполните все микрозадачи. Звучит немного запутанно, давайте проиллюстрируем фрагмент кода в начале статьи:

setTimeout(function() {
    console.log('setTimeout');
})
new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})
console.log('console');
  1. Этот код входит в основной поток как задача макроса.
  2. Когда setTimeout встречается первым, его функция обратного вызова регистрируется и распространяется на задачу макроса Event Queue. (Процесс регистрации такой же, как описано выше, и не будет описан ниже)
  3. Далее встречается Promise, немедленно выполняется new Promise(), а функция then распределяется по очереди событий микрозадачи.
  4. Когда встречается console.log(), он выполняется немедленно.
  5. Итак, общий код скрипта выполняется как первая задача макроса. Давайте посмотрим, какие есть микрозадачи? Мы обнаружили, что функция then находится в очереди событий микрозадачи, выполняет ее.
  6. ок, первый виток цикла событий закончен, начинаем второй виток цикла, естественно, начиная с макро-задачи Event Queue. Мы нашли callback-функцию, соответствующую setTimeout в макро-задаче Event Queue, и тут же ее выполнили.
  7. конец.

Связь между циклом событий, макрозадачей и микрозадачей показана на рисунке:

Давайте проанализируем более сложный фрагмент кода, чтобы увидеть, действительно ли вы владеете механизмом выполнения js:

console.log('1');
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

Первый раунд анализа процесса цикла событий выглядит следующим образом:

  • Общий сценарий входит в основной поток как первая задача макроса, сталкивается с console.log и выводит 1.
  • При обнаружении setTimeout его функция обратного вызова отправляется в очередь задач макроса Event Queue. давайте пока назовем его setTimeout1.
  • Когда встречается process.nextTick(), его функция обратного вызова отправляется в очередь событий микрозадачи. Мы записываем его как process1.
  • Когда встречается промис, new Promise() выполняется напрямую и выводит 7. Функция then отправляется в очередь событий микрозадачи. Назовем это тогда1.
  • Мы снова столкнулись с setTimeout, и его функция обратного вызова была распространена на макрозадачу Event Queue, которую мы записали как setTimeout2.

  • В приведенной выше таблице показано состояние каждой очереди событий в конце первого раунда макрозадач цикла событий, и в это время были выведены 1 и 7.
  • Мы нашли две микрозадачи, process1 и then1.
  • Выполнить процесс 1, вывод 6.
  • Выполнить then1, вывести 8.

Что ж, первый раунд цикла событий официально завершен. Результатом этого раунда является вывод 1, 7, 6 и 8. Затем начинается второй раунд временного цикла с макрозадачи setTimeout1:

  • Первый вывод 2. Затем был обнаружен process.nextTick(), который также был распределен в очередь событий микрозадачи, которая была записана как process2. new Promise() немедленно выполняет вывод 4, а функция then также распространяется на очередь событий микрозадачи, обозначенную как then2.

  • В конце второго раунда макрозадач цикла обработки событий мы обнаружили, что есть две микрозадачи process2 и then2, которые можно выполнить.
  • вывод 3.
  • вывод 5.
  • Второй раунд цикла событий заканчивается, и второй раунд выводит 2, 4, 3, 5.
  • Начинается третий раунд цикла событий, и в это время остается только setTimeout2, и он выполняется.
  • прямой выход 9
  • Распространите process.nextTick() в очередь событий микрозадачи. Запишите его как процесс3.
  • Выполните new Promise() напрямую, выведите 11.
  • Распространите функцию then на очередь событий микрозадачи, обозначенную как then3.

  • Третий раунд выполнения макрозадач цикла событий заканчивается, и выполняются две микрозадачи process3 и then3.
  • вывод 10.
  • вывод 12.
  • Третий раунд цикла событий заканчивается, и третий раунд выводит 9, 11, 10 и 12.

Весь код имеет в общей сложности три цикла событий, а полный вывод — 1, 7, 6, 8, 2, 4, 3, 5, 9, 11, 10, 12. (Обратите внимание, что мониторинг событий в узле среда зависит от libuv, а внешняя среда не совсем такая же, и могут быть ошибки в порядке вывода)

6. Пишите в конце

6.1 js асинхронный

Мы с самого начала говорили, что javascript — это однопоточный язык. Какой бы ни была так называемая асинхронность, реализованная новым фреймворком и новым синтаксическим сахаром, на самом деле она моделируется синхронным методом. Очень важно крепко ухватиться за нить.

6.2 Цикл событий

Цикл событий — это способ для js реализовать асинхронность, а также механизм выполнения js.

6.3 Выполнение и запуск javascript

Есть большая разница между исполнением и бегом. JavaScript выполняется по-разному в разных средах, таких как node, браузер, Ringo и т. д. Работа в основном относится к унифицированному механизму синтаксического анализа javascript.

6.4 установитьНемедленно

Существует много типов микро-задач и макро-задач, таких как setImmediate и т. д. Все выполнения имеют что-то общее, и заинтересованные друзья могут узнать о них сами.

6.5 последний из последних

Твердо усвойте два основных момента, серьезно сосредоточьтесь на изучении javascript и осуществите великую мечту стать мастером интерфейса как можно скорее!

  • javascript - это однопоточный язык
  • Цикл событий — это механизм выполнения javascript.