Во-первых, JavaScript — это однопоточный язык сценариев.
То есть это означает, что во время выполнения одной строки кода не должно быть другой строки кода, которая выполняется в то же время, как и при использованииalert()
сойти с ума позжеconsole.log
, если всплывающее окно не закрыто, консоль не будет отображать сообщениеlog
информационный.
Либо какой-то код выполняет множество вычислений, например, фантомные операции, такие как взлом пароля методом перебора на фронтенде, из-за чего последующий код будет все время ждать, а страница находится в состоянии анабиоза, потому что предыдущая код не был выполнен.
Так что, если весь код будет выполняться синхронно, это вызовет серьезные проблемы.Например, если мы хотим получить какие-то данные с удаленного конца, должны ли мы продолжать перебирать код, чтобы определить, получен ли возвращаемый результат?Это как пойти в ресторан заказывать еду, вы точно не можете сказать, что после заказа вы пойдете на заднюю кухню призывать людей готовить, и вас побьют.
Итак, есть концепция асинхронных событий, регистрируем callback-функцию, например отправку сетевого запроса, мы говорим основной программе подождать, пока данные не будут получены, и уведомить меня, а затем мы можем заняться другими делами.
Тогда после асинхронного завершения нас уведомят, но в это время программа может заниматься другими делами, поэтому даже если асинхронное завершение будет завершено, вам нужно подождать в стороне.Когда программа простаивает, вы успеете посмотреть какой асинхронник выполнен.можно переходить к реализации.
Например, если вы берете такси, если водитель подъезжает первым, а у вас еще есть какие-то дела, то водитель не может уехать первым, и он должен дождаться, пока вы закончите и сядете в машину раньше. уход.
Разница между микрозадачами и макрозадачами
Это как пойти в банк по делам: сначала нужно получить номер и договориться о номере.
Обычно что-то вроде: «Ваш номер ХХ, впереди ХХ человек» и так далее.
Поскольку функции кассира заключаются в том, чтобы в одно и то же время иметь дело с клиентом, который приходит для ведения бизнеса, то каждого человека, который приходит для ведения бизнеса, можно рассматривать как макрозадачу банковского кассира. запуск следующей задачи макроса.
Таким образом, сочетание нескольких макрозадач можно рассматривать как очередь задач, которая содержит всех клиентов в текущем банке.
Очередь задач заполнена выполненными асинхронными операциями, не то что регистрация асинхронной задачи будет помещена в очередь задач, так же как и расстановка номера в банке.Если вас нет на месте, когда вам звонят, то ваш текущий номерной знак недействительно, кассир решит пропустить бизнес-обработку следующего клиента, и вам нужно будет снова получить номер после того, как вы вернетесь
Кроме того, во время выполнения макрозадачи вы можете добавить некоторые микрозадачи, как и при ведении бизнеса за прилавком.Пожилой человек перед вами может вносить депозит.После завершения депозитного дела кассир попросите старика заплатить Есть ли какие-либо другие дела, которые нужно решить? В это время старик подумал некоторое время: «В последнее время было много взрывов P2P. Вы хотите выбрать более стабильное финансовое управление ?» Затем он сказал кассиру, что хочет заняться кое-какими делами по управлению финансами. В это время кассир Вы определенно не можете сказать старику: «Вы можете пойти в заднюю часть и взять номер, и снова встать в очередь. ".
Итак, изначально была ваша очередь заниматься делами, но потому старик временно добавил:Финансовый бизнес"И оттолкнуться.
Может быть, старик все еще думает после того, как закончил свое финансовое управлениеПолучить другую кредитную карту? илиКупите еще несколько памятных монет?
Какими бы ни были потребности, если кассир может справиться с этим за нее, он сделает эти вещи, прежде чем заниматься вашим бизнесом, что можно рассматривать как микрозадачи.
Это указывает на то, что:Твой дядя всегда будет твоим дядей
Если текущая микрозадача не выполнена, следующая макрозадача выполняться не будет.
Итак, фрагмент кода, который часто используется в вопросах на собеседованиях и в различных блогах:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
setTimeout
существует как задача макроса, иPromise.then
Это репрезентативная микрозадача, и порядок выполнения приведенного выше кода выводится в соответствии с серийным номером.
Вся асинхронность, которая войдет, относится к той части кода в обратном вызове события.
то естьnew Promise
Код, выполняемый в процессе создания экземпляра, выполняется синхронно, иthen
Обратные вызовы, зарегистрированные в, выполняются асинхронно.
После выполнения синхронного кода вернитесь, чтобы проверить, завершена ли асинхронная задача, и выполните соответствующий обратный вызов, и микрозадача будет выполнена перед макрозадачей.
Таким образом, приведенный выше выходной вывод получается1、2、3、4
.
+ Синхронное исполнение кодовой части представляет
+setTimeout(_ => {
- console.log(4)
+})
+new Promise(resolve => {
+ resolve()
+ console.log(1)
+}).then(_ => {
- console.log(3)
+})
+console.log(2)
изначальноsetTimeout
Сначала был установлен таймер (эквивалентно взятию числа), а затем некоторые из них добавляются в текущий процесс.Promise
процессинг (временное добавление услуг).
Настолько продвинутый, даже если мы продолжимPromise
экземпляр вPromise
, его вывод все равно будет раньше, чемsetTimeout
Задача макроса:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
Promise.resolve().then(_ => {
console.log('before timeout')
}).then(_ => {
Promise.resolve().then(_ => {
console.log('also before timeout')
})
})
})
console.log(2)
Конечно, на практике таких простых вызовов очень мало.Promise
Да, в нем обычно есть и другие асинхронные операции, напримерfetch
,fs.readFile
такие операции.
И это фактически эквивалентно регистрации макрозадачи, а не микрозадачи.
P.S. вСпецификация Promise/A+середина,Promise
Реализация может быть как микрозадачей, так и макрозадачей, но представление общего консенсуса (по крайней мере,Chrome
готово),Promise
Он должен принадлежать к лагерю микрозадач
Поэтому очень важно понимать, какие операции являются макрозадачами, а какие — микрозадачами.В отрасли популярна поговорка:
задача макроса
# | браузер | Node |
---|---|---|
I/O |
✅ | ✅ |
setTimeout |
✅ | ✅ |
setInterval |
✅ | ✅ |
setImmediate |
❌ | ✅ |
requestAnimationFrame |
✅ | ❌ |
Некоторые места будут перечисленыUI Rendering
, сказал, что это тоже задача макроса, но прочитавДокумент спецификации HTMLПозже выяснилось, что это явно шаг операции параллельно с микрозадачей.
requestAnimationFrame
Скажем так, это макрозадача.requestAnimationFrame
существуетОпределение MDNэто операция, выполняемая перед перерисовкой следующей страницы, и перерисовка также существует как шаг макрозадачи, и этот шаг позже, чем выполнение микрозадачи
микрозадачи
# | браузер | Node |
---|---|---|
process.nextTick |
❌ | ✅ |
MutationObserver |
✅ | ❌ |
Promise.then catch finally |
✅ | ✅ |
Что такое цикл событий
Выше обсуждалось выполнение макрозадач, микрозадач и различных задач.
Но вернемся к реальности,JavaScript
Это язык с одним процессом, и он не может обрабатывать несколько задач одновременно, поэтому когда вы выполняете макрозадачи, а когда — микрозадачи? Нам нужно, чтобы такая логика суждения существовала.
Каждый раз, когда бизнес завершается, кассир спрашивает текущего клиента, есть ли какие-либо другие дела, которые необходимо решить.(Проверьте, есть ли микрозадачи для работы)
После того, как клиент четко сообщил, что делать нечего, кассир пошел проверить, есть ли еще люди, ожидающие, чтобы заняться бизнесом.(Завершите эту задачу макроса и проверьте, есть ли какие-либо задачи макроса, которые нужно обработать)
Процесс этой проверки является непрерывным, и он будет выполняться один раз при каждом завершении задачи, и такая операция называетсяEvent Loop
.(Это очень простое описание, на деле будет намного сложнее)
И как упоминалось выше, кассир может обрабатывать только одну вещь за раз, даже если эти вещи предлагает клиент, поэтому можно считать, что есть еще и очередь на микрозадачи, что примерно так и логично:
const macroTaskList = [
['task1'],
['task2', 'task3'],
['task4'],
]
for (let macroIndex = 0; macroIndex < macroTaskList.length; macroIndex++) {
const microTaskList = macroTaskList[macroIndex]
for (let microIndex = 0; microIndex < microTaskList.length; microIndex++) {
const microTask = microTaskList[microIndex]
// 添加一个微任务
if (microIndex === 1) microTaskList.push('special micro task')
// 执行任务
console.log(microTask)
}
// 添加一个宏任务
if (macroIndex === 2) macroTaskList.push(['special macro task'])
}
// > task1
// > task2
// > task3
// > special micro task
// > task4
// > special macro task
Причина использования двухfor
Он представлен петлей, потому что его очень удобно выполнять внутри петли.push
Такие операции (добавление каких-то задач), чтобы количество итераций динамически увеличивалось.
И чтобы было ясно,Event Loop
Он отвечает только за то, чтобы сообщить вам, какие задачи выполнять или какие обратные вызовы запускаются, реальная логика все еще выполняется в процессе.
Производительность в браузере
Разница между этими двумя задачами кратко описана выше, иEvent Loop
Так как же это выглядит в реальном браузере?
Первое, что нужно уяснить, это то, что макрозадача должна выполняться после микрозадачи (поскольку микрозадача фактически является одним из шагов макрозадачи).
I/O
Это кажется немного общим, слишком много вещей, чтобы назвать этоI/O
, нажмите один разbutton
, загрузку файла и взаимодействие с программой можно назватьI/O
.
Допустим есть такиеDOM
структура:
<style>
#outer {
padding: 20px;
background: #616161;
}
#inner {
width: 100px;
height: 100px;
background: #757575;
}
</style>
<div id="outer">
<div id="inner"></div>
</div>
const $inner = document.querySelector('#inner')
const $outer = document.querySelector('#outer')
function handler () {
console.log('click') // 直接输出
Promise.resolve().then(_ => console.log('promise')) // 注册微任务
setTimeout(_ => console.log('timeout')) // 注册宏任务
requestAnimationFrame(_ => console.log('animationFrame')) // 注册宏任务
$outer.setAttribute('data-random', Math.random()) // DOM属性修改,触发微任务
}
new MutationObserver(_ => {
console.log('observer')
}).observe($outer, {
attributes: true
})
$inner.addEventListener('click', handler)
$outer.addEventListener('click', handler)
Если вы нажмете#inner
, порядок выполнения должен быть:click
-> promise
-> observer
-> click
-> promise
-> observer
-> animationFrame
-> animationFrame
-> timeout
-> timeout
.
потому что однаждыI/O
Создал задачу макроса, а это значит, что она будет срабатывать в этой задачеhandler
.
Судя по комментариям в коде, после выполнения синхронизируемого кода пора проверить, есть ли микрозадачи, которые можно выполнить, а затем найтиPromise
а такжеMutationObserver
Выполняются две микрозадачи.
потому чтоclick
События будут пузыриться, так что на этот раз соответствующиеI/O
сработает дваждыhandler
функция(однажды вinner
, однажды вouter
), поэтому всплывающее событие будет выполнено первым (Перед другими макрозадачами), что означает, что описанная выше логика будет повторяться.
После выполнения кода синхронизации и микрозадач продолжайте поиск любых макрозадач в обратном направлении.
Следует отметить, что, поскольку мы запускаемsetAttribute
, который фактически изменяетDOM
свойство, которое вызывает перерисовку страницы, и этоset
операции выполняются синхронно, т.requestAnimationFrame
Обратный вызов будет раньше, чемsetTimeout
выполнено.
несколько маленьких сюрпризов
Используя приведенный выше пример кода, если вы вручную нажметеDOM
Метод триггера элемента становится$inner.click()
, то будут получены разные результаты.
существуетChrome
Последовательность вывода ниже примерно следующая:
click
-> click
-> promise
-> observer
-> promise
-> animationFrame
-> animationFrame
-> timeout
-> timeout
.
Триггер вручную с намиclick
Причина разного порядка выполнения заключается в том, что не триггерное событие реализуется пользователем при нажатии на элемент, аdispatchEvent
Я не думаю, что этот подход эффективен.I/O
после выполнения один разhandler
Обратный вызов зарегистрировал микрозадачу, после регистрации макрозадачи, фактически снаружи$inner.click()
не закончено.
Следовательно, прежде чем микрозадача будет выполнена, она должна продолжать всплывать, чтобы выполнить следующее событие, то есть сработать второе событие.handler
.
Так выведите второй разclick
, подождите, пока эти два разаhandler
После того, как все выполнения будут завершены, он проверит, есть ли микрозадачи и есть ли макрозадачи.
Два момента, на которые следует обратить внимание:
-
.click()
Этот способ запуска события лично думает, что он похож наdispatchEvent
, что можно понимать как код, выполняемый синхронно
document.body.addEventListener('click', _ => console.log('click'))
document.body.click()
document.body.dispatchEvent(new Event('click'))
console.log('done')
// > click
// > click
// > done
-
MutationObserver
Слушатель не скажет, что он срабатывает несколько раз одновременно, и только один обратный вызов будет срабатывать для нескольких модификаций.
new MutationObserver(_ => {
console.log('observer')
// 如果在这输出DOM的data-random属性,必然是最后一次的值,不解释了
}).observe(document.body, {
attributes: true
})
document.body.setAttribute('data-random', Math.random())
document.body.setAttribute('data-random', Math.random())
document.body.setAttribute('data-random', Math.random())
// 只会输出一次 ovserver
Это как пойти в ресторан, чтобы заказать еду, а официант трижды крикнул: говяжья лапша номер ХХ, это не значит, что она даст вам три тарелки говяжьей лапши.
Вышеперечисленные точки зрения относятся кTasks, microtasks, queues and schedules, в тексте есть анимированная версия пояснения
Производительность в узле
Node тоже однопоточный, но обработкаEvent Loop
Он немного отличается от браузера, вотОфициальная документация узлаадрес г.
Что касается уровня API, Node добавил два новых метода, которые можно использовать:process.nextTick
и макро задачиsetImmediate
.
Разница между setImmediate и setTimeout
Как указано в официальной документации,setImmediate
однаждыEvent Loop
Вызывается после завершения выполнения.
setTimeout
Он выполняется путем вычисления времени задержки.
Но также упоминается, что если эти две операции выполняются непосредственно в основном процессе, трудно гарантировать, какая из них сработает первой.
Потому что если в основном процессе сначала регистрируются две задачи, то код для выполнения занимает больше времени, чемXXs
, а таймер уже находится в состоянии, когда обратный вызов может быть выполнен.
Таким образом, таймер будет выполняться первым, а после того, как таймер будет выполнен, он завершится один раз.Event Loop
, то он будет выполнятьсяsetImmediate
.
setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))
Если вам интересно, вы можете попробовать это сами, и вы получите разные результаты, если будете выполнять его несколько раз.
Но после последующего добавления некоторых кода вы можете гарантироватьsetTimeout
Будет вsetImmediate
Запущено до:
setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))
let countdown = 1e9
while(countdown--) { } // 我们确保这个循环的执行速度会超过定时器的倒计时,导致这轮循环没有结束时,setTimeout已经可以执行回调了,所以会先执行`setTimeout`再结束这一轮循环,也就是说开始执行`setImmediate`
Если в другой задаче макроса, он должен бытьsetImmediate
Выполнить сначала:
require('fs').readFile(__dirname, _ => {
setTimeout(_ => console.log('timeout'))
setImmediate(_ => console.log('immediate'))
})
// 如果使用一个设置了延迟的setTimeout也可以实现相同的效果
process.nextTick
Как упоминалось выше, это можно рассматривать какPromise
а такжеMutationObserver
реализация микрозадач, которые можно вставлять в любой момент выполнения кодаnextTick
, и гарантированно будет выполнен до запуска следующей задачи макроса.
Одним из наиболее распространенных примеров с точки зрения использования являются операции некоторых классов привязки событий:
class Lib extends require('events').EventEmitter {
constructor () {
super()
this.emit('init')
}
}
const lib = new Lib()
lib.on('init', _ => {
// 这里将永远不会执行
console.log('init!')
})
Поскольку приведенный выше код создает экземплярLib
Объект выполняется синхронно и отправляется сразу после завершения создания экземпляра.init
мероприятие.
В это время основная программа во внешнем слое еще не начала выполняться.lib.on('init')
Этот шаг прослушивания событий.
Поэтому при отправке события обратного вызова не будет, и событие не будет отправлено повторно после регистрации обратного вызова.
Мы можем легко использоватьprocess.nextTick
Для решения этой проблемы:
class Lib extends require('events').EventEmitter {
constructor () {
super()
process.nextTick(_ => {
this.emit('init')
})
// 同理使用其他的微任务
// 比如Promise.resolve().then(_ => this.emit('init'))
// 也可以实现相同的效果
}
}
Это сработает, когда программа простаивает после выполнения кода основного процесса.Event Loop
Процесс находит, есть ли микрозадачи, а затем отправляет ихinit
мероприятие.
Как упоминалось в некоторых статьях, цикл вызываетprocess.nextTick
Это вызвало сигнал тревоги, а последующий код никогда не будет выполнен, что правильно, см. Реализацию двойного петля, используемого вышеloop
, что эквивалентно каждый разfor
Массив выполняется в цикле выполненияpush
операция, чтобы цикл никогда не заканчивался
Несколько слов об асинхронных/ожидающих функциях
потому что,async/await
в основном на основеPromise
некоторые пакеты, в то время какPromise
Это своего рода микрозадача. Итак, используяawait
ключевое слово сPromise.then
Эффект аналогичен:
setTimeout(_ => console.log(4))
async function main() {
console.log(1)
await Promise.resolve()
console.log(3)
}
main()
console.log(2)
Код асинхронной функции перед ожиданием выполняется синхронно, что можно понимать как код перед ожиданием, принадлежащийnew Promise
Когда код передается, весь код после ожидания находится вPromise.then
обратный вызов в
подраздел
В Интернете написано много статей о механизме выполнения кода в JavaScript, я слишком поверхностен, поэтому могу лишь кратко рассказать о своем понимании этого механизма.
Я не стал переходить к сырым документам, а перечислил их пошагово, вроде просмотра текущего стека, выполнения выбранной очереди задач и разных балабал.
Я чувствую, что на самом деле писать код не очень полезно, лучше просто войти в дверь, просканировать слепоту и иметь общее представление о том, что это такое.
Рекомендуемые статьи к прочтению:
- tasks-microtasks-queues-and-schedules
- understanding-js-the-event-loop
- Понимание process.nextTick() в Node.js
- Документация EventLoop в браузере
- Документация EventLoop в Node
- requestAnimationFrame | MDN
- MutationObserver | MDN
One more things
Команда Blued front-end/Node набирает сотрудников. . Неполная средняя школа и средняя школа имеют HC
Координаты Чаоян Шуанцзин, имперской столицы, если интересно, свяжитесь со мной:
чат: github_jiasm
Почта:jiashunming@blued.com
Добро пожаловать, чтобы оставить свое резюме