JavaScript (сокращенно JS) — это основной исследовательский язык для внешнего интерфейса.Если вы хотите по-настоящему понять JavaScript, вам не обойтись без его рабочего механизма — Event Loop.
JS — это однопоточный язык, и асинхронные операции являются важной частью практических приложений.Информацию об асинхронных операциях см. в другой моей статье.js история асинхронной разработки и анализ принципа PromiseЯ не буду здесь вдаваться в подробности.
куча, стопка, очередь
куча
Куча относится к динамической памяти, запрашиваемой программой во время ее работы, и используется для хранения объектов во время работы JS.
куча
Стек следует принципу "первым пришел, последним вышел". Основные типы данных JS и адрес объекта хранятся в памяти стека. Кроме того, существует стековая память, используемая для выполнения основного потока JS - стек выполнения (контекст выполнения).стек), стек в этой статье рассматривает только стек выполнения.
очередь
Очередь работает по принципу «первым пришел, первым вышел».Помимо основного потока в JS существует еще и «очередь задач» (на самом деле их две, о чем будет подробно рассказано позже).
Event Loop
Один поток JS означает, что все задачи должны быть поставлены в очередь для выполнения в соответствии с определенными правилами, Это правило представляет собой цикл событий цикла событий, который мы хотим объяснить. Циклы событий работают по-разному в разных средах выполнения.
Цикл событий в среде браузера
Первая картинка (воспроизведена из выступления Филипа Робертса)Help, I'm stuck in an event-loop")
- Когда основной поток запускается, JS генерирует кучу и стек (стек выполнения)
- Пока асинхронная операция (событие dom, обратный вызов ajax, таймер и т. д.), сгенерированная веб-сайтом, вызываемым в основном потоке, дает результат, обратный вызов помещается в «очередь задач» для выполнения.
- Когда задачи синхронизации в основном потоке выполняются, система будет последовательно считывать задачи в «очереди задач» и помещать задачи в стек выполнения для выполнения.
- При выполнении задач также могут генерироваться новые асинхронные операции, которые будут генерировать новые циклы, и весь процесс непрерывен.
Из цикла событий нетрудно увидеть, что когда мы вызываем setTimeout и устанавливаем определенное время, фактическое время выполнения этой задачи может быть больше, чем время, которое мы установили, потому что задачи в основном потоке не были выполнены, что приводит к неточные таймеры, что также является причиной того, что непрерывный вызов setTimeout и вызов setInterval будут иметь разные эффекты (это не будет здесь расширяться, я напишу отдельную статью, когда у меня будет время).
Далее по коду:
console.log(1);
console.log(2);
setTimeout(function(){
console.log(3)
setTimeout(function(){
console.log(6);
})
},0)
setTimeout(function(){
console.log(4);
setTimeout(function(){
console.log(7);
})
},0)
console.log(5)
Время setTimeout в коде равно 0, что эквивалентно 4 мс, и может быть больше 4 мс (не важно). На что нам нужно обратить внимание, так это на порядок, в котором выводится код. Мы называем задачу числом, которое она выводит. Синхронный код должен выполняться первым, выводить сначала 1, 2 и 5, а задачи 3 и 4 будут по очереди входить в «очередь задач». После выполнения кода синхронизации 3 в очереди войдут в стек выполнения для выполнения, 4 дойдут до начала очереди, а после выполнения 3 внутренний setTimeout поставит задачу 6 в конец очереди. Начать 4 миссии...
В итоге мы получаем 1, 2, 5, 3, 4, 6, 7.
Макрозадачи и микрозадачи
Будут ли все задачи в очереди задач послушно поставлены в очередь? Ответ нет, задачи тоже разные, всегда есть задачи, которые имеют какие-то привилегии (типа нарезка очереди), то есть вип в задаче-микрозадача (микрозадача), те что без привилегий- -макрозадача (макрозадача). Давайте посмотрим на кусок кода:
console.log(1);
setTimeout(function(){
console.log(2);
Promise.resolve(1).then(function(){
console.log('promise')
})
})
setTimeout(function(){
console.log(3);
})
Согласно «теории очередей», результат должен быть 1, 2, 3, обещание. Но фактический результат имел неприятные последствия, и вывод был 1, 2, обещание, 3.
Очевидно, что очередь входит первой, почему обещание выводится впереди? Это связано с тем, что промисы имеют привилегию быть микрозадачами.Когда задача основного потока завершена, микрозадачи будут выполняться раньше макрозадач, независимо от того, появятся ли они позже.
Другими словами, на самом деле есть две очереди задач, одна очередь макрозадач, а другая очередь микрозадач, Когда основной поток выполняется, если в очереди микрозадач есть микрозадача, она войдет в выполнение сначала стек, а когда очередь микрозадач будет завершена, она войдет в стек выполнения первой.Очередь макрозадач будет выполняться только тогда, когда нет задач.
К микрозадачам относятся: Native Promise (некоторые реализованные промисы помещают метод then в макрозадачи), Object.observe (устарело), MutationObserver, MessageChannel;
Задачи макроса включают: setTimeout, setInterval, setImmediate, I/O;
Цикл событий в среде узла
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
Цикл времени в узле не такой, как в браузере, как показано на рисунке:
- этап таймеров: на этом этапе выполняются запланированные обратные вызовы setTimeout (обратный вызов) и setInterval (обратный вызов);
- Этап обратных вызовов ввода-вывода: выполнение обратных вызовов, отличных от обратных вызовов события закрытия, обратных вызовов, установленных таймерами (таймеры, setTimeout, setInterval и т. д.), и обратных вызовов, установленных setImmediate();
- стадия ожидания, подготовка: используется только узлом внутри;
- Фаза опроса: получение новых событий ввода-вывода, узел будет заблокирован здесь при соответствующих условиях;
- этап проверки: выполнить обратные вызовы, заданные setImmediate();
- этап закрытых обратных вызовов: например, на этом этапе будет выполнен обратный вызов socket.on('close', callback).
У каждого этапа есть очередь fifo (очередь) с обратными вызовами.Когда цикл событий доходит до указанного этапа, Узел будет выполнять очередь fifo (очередь) этого этапа.При выполнении обратного вызова очереди или количестве выполненных обратных вызовов превышает верхний предел этапа, Цикл событий перейдет к следующему этапу.
process.nextTick
Метода process.nextTick нет в приведенном выше цикле событий, мы можем понимать его как микрозадачу, а время его выполнения — хвост текущего «стека выполнения» — следующий цикл событий (основной поток читает "очередь задач") До ---- запуска функции обратного вызова. То есть указанная задача всегда выполняется перед всеми асинхронными задачами. Метод setImmediate добавляет событие в конец текущей «очереди задач», то есть указанная им задача всегда выполняется в следующем цикле событий. Над кодом:
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
Из кода видно, что не только функция A выполняется до истечения времени ожидания функции обратного вызова, указанного setTimeout, но и функция B выполняется до истечения времени ожидания. Это означает, что если имеется несколько операторов process.nextTick (независимо от того, являются ли они вложенными или нет), все они будут выполняться в текущем «стеке выполнения».
setTimeout и setImmediate
Они очень похожи, но разница между ними зависит от того, когда они вызываются.
- Дизайн setImmediate выполняется, когда фаза опроса завершена, то есть фаза проверки;
- setTimeout предназначен для выполнения, когда этап опроса бездействует, и выполняется после наступления установленного времени; но он выполняется на этапе таймера.
Порядок вызова этих двух функций зависит от контекста текущего цикла обработки событий.Если они вызываются вне асинхронного обратного вызова ввода-вывода, порядок выполнения неизвестен.
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
Это связано с тем, что цикл событий может находиться на разных стадиях, когда происходит последнее событие, что приводит к неопределенным результатам. Когда мы придаем циклу событий определенный контекст, можно определить последовательность событий.
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
$ node timeout_vs_immediate.js
immediate
timeout
Это связано с тем, что после выполнения обратного вызова fs.readFile программа устанавливает таймер и setImmediate, поэтому этап опроса не будет заблокирован, а затем переходит на этап проверки, чтобы сначала выполнить setImmediate, а затем переходит на этап таймера, чтобы выполнить setTimeout.