недавно учусьVue
Исходный код, только что изучил асинхронное обновление виртуального DOM, которое тут задействованоJavaScript
цикл событий вEvent Loop
. Раньше я еще смутно относился к этому понятию, я, наверное, знал, что это такое, но не изучал его глубоко. Просто воспользовался этой возможностью, чтобы вернуться и узнатьEvent Loop
.
JavaScript — это однопоточный язык
цикл событийEvent Loop
, который является текущим браузером иNodeJS
иметь дело сJavaScript
Механизм кода, и за существованием этого механизма стоит потому, чтоJavaScript
это дверьодин потокязык.
Самое простое различие между однопоточностью и многопоточностью заключается в том, что однопоточность может выполнять только одно действие одновременно, тогда как многопоточность может выполнять несколько действий одновременно.
иJavaScript
Так называемый дизайн — это однопоточный язык, главным образом потому, что это язык сценариев браузера, и его основная цель — взаимодействовать с пользователями, управлятьDom
узел.
В этом сценарии, предполагаяJavaScript
Одновременно выполняются два процесса: один — управлять узлом A, а другой — удалять узел A. В настоящее время браузер не знает, какой поток использовать.
Поэтому, чтобы избежать подобных проблем,JavaScript
Это был однопоточный язык с самого начала.
Стек вызовов
существуетJavaScript
При запуске основной поток формирует стек, который в основном является механизмом, используемым интерпретатором для конечного потока выполнения функции. Обычно этот стек называют стеком вызововCall Stack
или стек выполнения (Execution Context Stack
).
Стек вызовов, как следует из названия, представляет собой структуру с принципом LIFO (последний вошел, первый вышел, последний вошел, первый вышел). Внутри стека вызовов находятся все контексты выполнения во время выполнения кода.
- Каждый раз, когда вызывается функция, интерпретатор добавляет контекст выполнения функции в стек вызовов и начинает выполнение;
- Функция, которая выполняется в стеке вызовов, если также вызываются другие функции, новая функция также будет добавлена в стек вызовов и выполнена немедленно;
- После выполнения текущей функции интерпретатор удалит свой контекст выполнения из стека вызовов и продолжит выполнение оставшегося кода в оставшемся контексте выполнения;
- Но выделенное пространство стека вызовов заполнено, что вызовет ошибку «переполнение стека».
Теперь используйте небольшой пример, чтобы продемонстрировать стек вызовов.
function a() {
console.log('a');
}
function b() {
console.log('b');
}
function c() {
console.log('c');
a();
b();
}
c();
/**
* 输出结果:c a b
*/
При выполнении этого кода вызывается первая функцияc()
. следовательноfunction c(){}
Контекст выполнения помещается в стек вызовов.
Затем начните выполнять функциюc
, первая выполняемая инструкцияconsole.log('c')
.
Поэтому интерпретатор также помещает его в стек вызовов.
когдаconsole.log('c')
После выполнения метода консоль выводит'c'
, стек вызовов удалит его.
затем выполнитьa()
функция.
переводчик будетfunction a() {}
Контекст выполнения помещается в стек вызовов.
выполнить немедленноa()
Предложение в -console.log('a')
.
когда функцияa
После выполнения стек вызовов удаляет контекст выполнения.
а затем выполнитьc()
Остальные операторы функции, то есть выполнениеb()
функция, поэтому ее контекст выполнения добавляется в стек вызовов.
выполнить немедленноb()
Предложение в -console.log('b')
.
b()
После выполнения стек вызовов удалит его.
В настоящее времяc()
Выполнение также завершено, и стек вызовов также удален из стека.
На этом наше заявление заканчивается.
очередь задач
Вышеупомянутый случай кратко представляетJavaScript
Однопоточное выполнение.
Но в этом есть некоторые проблемы, то есть, если оператор выполняется долго, например, запрос данных, таймеров, чтение файлов и т. д., последующие операторы должны ждать, пока не завершится выполнение предыдущего оператора. Начать выполнение.
Очевидно, что это нежелательно.
Синхронные и асинхронные задачи
следовательно,JavaScript
Разделите все задачи выполнения на синхронные задачи и асинхронные задачи.
На самом деле, каждая из наших задач делает две вещи, т.позвонитьиполучил ответ.
Основное отличие синхронной задачи от асинхронной состоит в том, что после того, как синхронная задача инициирует вызов, результат может быть получен в ближайшее время, в то время как асинхронная задача не может получить результат сразу, например запрос интерфейса, каждый интерфейс будет иметь определенный время отклика, в зависимости от сети. Оно определяется такими факторами, как скорость, сервер и т. д. Другим примером является таймер, который принимает фиксированное время, прежде чем вернуть результат.
Следовательно, механизм выполнения синхронных задач и асинхронных задач также отличается.
Выполнение задачи синхронизации фактически такое же, как и в предыдущем случае, по последовательности кода и последовательности вызова поддерживает вход в стек вызовов и его выполнение, после выполнения стек вызовов удаляется.
Для выполнения асинхронной задачи она все равно сначала войдет в стек вызовов, а потом инициирует вызов, а потом его вызовет интерпретаторОтветная задача обратного вызоваположить вочередь задач, то стек вызовов удалит задачу. Когда основной поток опустеет, то есть после завершения всех задач синхронизации, интерпретатор прочитает очередь задач и последовательноЗавершенные асинхронные задачиДобавьте в стек вызовов и выполните.
Важным моментом здесь является то, что асинхронные задачи не попадают в очередь задач напрямую.
Вот простой пример.
console.log(1);
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json))
console.log(2);
очевидно,fetch()
является асинхронной задачей.
но выполнитьconsole.log(2)
Прежде, на самом делеfetch()
Позвонили и сделали запрос, но не ответили с данными. И функции, которые реагируют на данные и обрабатывают данныеthen()
уже в это времяочередь задачв ожиданииconsole.log(2)
После завершения выполнения, после очистки задачи синхронизации, она входит в стек вызовов для выполнения ответного действия.
Макрозадачи и микрозадачи
Говоря о синхронных задачах и асинхронных задачах ранее, я упомянулочередь задач.
В очереди задач она фактически делится наОчередь макросовиОчередь микрозадач, соответствующий хранящийся внутризадача макросаимикрозадачи.
Во-первых,И макрозадачи, и микрозадачи являются асинхронными задачами.
Разница между макрозадачами и микрозадачами заключается в порядке их выполнения, поэтому различают макрозадачи и микрозадачи.
В синхронных задачах выполнение задач выполняется в порядке кода, а выполнение асинхронных задач также должно выполняться в порядке.Атрибуты очереди:First in First Out (FIFO, First in First Out), поэтому асинхронные задачи выполняются в том порядке, в котором они были поставлены в очередь.
Но в некоторых сценариях будут проблемы, если он будет выполняться только в том порядке, в котором он введен в очередь. Например, очередь сначала входит в часовой таймер, а затем входит в функцию интерфейса запроса.Если она выполняется в соответствии с порядком входа в очередь, функции интерфейса запроса может потребоваться час, чтобы ответить на данные.
Поэтому браузер будет делить асинхронные задачи на макрозадачи и микрозадачи, а затем выполнять их по механизму цикла событий, поэтому разные задачи будут иметь разные приоритеты выполнения, о чем и пойдет речь в цикле событий.
поставить задачу в очередь
Здесь есть еще один пункт знаний, который касается задачи присоединения к команде.
Когда задача попадает в очередь задач, она фактически использует другие потоки браузера. Несмотря на тоJavaScript
является однопоточным языком, но браузеры не являются однопоточными. Разные потоки будут обрабатывать разные события, и когда соответствующее событие может быть выполнено, соответствующий поток поставит его в очередь задач.
- поток движка js: используется для интерпретации и выполнения кода js, пользовательского ввода, сетевых запросов и т. д.;
- Поток рендеринга графического интерфейса: рисовать пользовательский интерфейс, взаимоисключающий основной поток JS (поскольку JS может манипулировать DOM, что, в свою очередь, повлияет на результат рендеринга GUI);
- HTTP поток асинхронных сетевых запросов: обрабатывать запросы пользователя на получение, отправку и другие запросы и помещать функцию обратного вызова в очередь задач после возврата результата;
-
синхронизированный триггерный поток:
setInterval
,setTimeout
По истечении времени ожидания функция выполнения будет помещена в очередь задач; -
Поток обработчика событий браузера:будет
click
,mouse
После того, как происходит событие взаимодействия с пользовательским интерфейсом, функция обратного вызова, которая должна быть выполнена, помещается в очередь событий.
Это на самом деле объясняет, почему следующий код будет запускать следующий таймер раньше предыдущего. Потому что последний таймер будет помещен в очередь задач макросов первым, а первый будет помещен в очередь задач макросов после истечения времени таймера.
setTimeout(() => {
console.log('a');
}, 10000);
setTimeout(() => {
console.log('b');
}, 100);
задача макроса
браузер | Node | |
---|---|---|
Общий код (скрипт) | ✅ | ✅ |
События взаимодействия с пользовательским интерфейсом | ✅ | ❌ |
I/O | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
requestAnimationFrame | ✅ | ❌ |
микрозадачи
браузер | Node | |
---|---|---|
process.nextTick | ❌ | ✅ |
MutationObserver | ✅ | ❌ |
Promise.then catch finally | ✅ | ✅ |
Цикл событий Цикл событий
По сути, выполнение очереди макро-задач и очереди микро-задач является частью цикла обработки событий, поэтому я расскажу об этом здесь.
Конкретный процесс цикла событий выглядит следующим образом:
- Из очереди задач макроса следуйтеПоставить в очередь заказ, найти первую макрозадачу для выполнения, поместить ее в стек вызовов и начать выполнение;
- законченныйЗадача макросаПосле загрузки всех задач синхронизации, то есть после очистки стека вызовов, макрозадача выталкивается из очереди макрозадач, а затем очередь микрозадач начинает выполнять микрозадачи последовательно в соответствии с порядком входа.пока очередь микрозадач не опустеет;
- Когда очередь микрозадач очищается, цикл событий завершается;
- Затем в очереди задач макроса найдите следующую задачу макроса для выполнения и запустите второй цикл обработки событий, пока очередь задач макроса не будет очищена.
Вот несколько основных моментов:
- Когда мы выполним его в первый раз, интерпретатор
script
Поместить в очередь задач макроса, чтобы цикл обработки событий начинался с первой задачи макроса; - Если новая микрозадача генерируется и добавляется в очередь микрозадач во время выполнения микрозадачи, ее также необходимо очистить вместе, следующая макрозадача не будет выполняться до тех пор, пока очередь микрозадач не будет очищена.
Затем смоделируйте цикл событий на примере распространенного вопроса интервью.
console.log("a");
setTimeout(function () {
console.log("b");
}, 0);
new Promise((resolve) => {
console.log("c");
resolve();
})
.then(function () {
console.log("d");
})
.then(function () {
console.log("e");
});
console.log("f");
/**
* 输出结果:a c f d e b
*/
Во-первых, когда код выполняется, весь кодscript
помещается в очередь задач макроса и начинает выполнение задачи макроса.
В порядке кода выполнить сначалаconsole.log("a")
.
Контекст функции помещается в стек вызовов, и после выполнения стек вызовов удаляется.
выполнить следующийsetTimeout()
, контекст функции также входит в стек вызовов.
так какsetTimeout
это задача макроса, так что сделайте этоcallback
Функция помещается в очередь задач макроса, затем функция удаляется из стека вызовов, и выполнение продолжается вниз.
с последующимPromise
оператора, сначала поместите его в стек вызовов, а затем выполните его вниз.
воплощать в жизньconsole.log("c")
иresolve()
, тут особо нечего сказать.
Потомnew Promise().then()
метод, это микрозадача, поэтому поместите ее в очередь микрозадач.
В настоящее времяnew Promise
Когда оператор завершает выполнение, он удаляется из стека вызовов.
Затем выполните выполнениеconsole.log('f')
.
В этот момент,script
Выполнение задачи макроса завершено, поэтому она выталкивается из очереди задач макроса.
Затем начните очищать очередь микрозадач. Первое, что нужно сделать, этоPromise then
, поэтому он помещается в стек вызовов.
а затем начать выполнениеconsole.log("d")
.
После завершения выполнения обнаруживается, что есть еще одинthen()
функцию, поэтому поместите ее в очередь микрозадач.
В это время первыйthen()
Когда функция завершает выполнение, стек вызовов и очередь микрозадач удаляются.
На данный момент очередь микрозадач не была очищена, поэтому продолжайте выполнять следующую микрозадачу.
Процесс выполнения аналогичен предыдущему, поэтому много говорить не буду.
В этот момент очередь микрозадач была очищена, и первый цикл обработки событий завершился.
Далее выполняется следующая макрозадача, т.е.setTimeout callback
.
После выполнения он также удаляется из очереди задач макроса и стека вызовов.
В этот момент в очереди микрозадач нет задач, поэтому второй цикл обработки событий также завершается.
Задача макроса также очищается, поэтому выполнение этого кода завершено.
await
Добавлено в ECMAScript2017async functions
иawait
.
async
Ключевое слово — превратить синхронную функцию в асинхронную и изменить возвращаемое значение наpromise
.
иawait
может быть помещен в любой асинхронный, основанныйpromise
перед функцией. Во время выполнения он приостанавливает код в этой строке до тех пор, покаpromise
Готово, затем верните значение результата. Во время паузы другой код, ожидающий выполнения, имеет шанс выполниться.
Давайте попробуем это на примере.
async function async1() {
console.log("a");
const res = await async2();
console.log("b");
}
async function async2() {
console.log("c");
return 2;
}
console.log("d");
setTimeout(() => {
console.log("e");
}, 0);
async1().then(res => {
console.log("f")
})
new Promise((resolve) => {
console.log("g");
resolve();
}).then(() => {
console.log("h");
});
console.log("i");
/**
* 输出结果:d a c g i b h f e
*/
Во-первых, перед началом выполнения поместите общий кодscript
Поместите в очередь задач макроса и начните выполнение.
Первое, что нужно выполнить, этоconsole.log("d")
.
с последующимsetTimeout
, поместите его обратный вызов в задачу макроса и продолжите выполнение.
с последующим вызовомasync1()
function, тем самым помещая контекст своей функции в стек вызовов.
затем начните выполнениеasync1
серединаconsole.log("a")
.
Далееawait
утверждение ключевого слова.
await
Более поздний звонокasync2
функцию, поэтому мы помещаем ее в стек вызовов.
затем начните выполнениеasync2
серединаconsole.log("c")
,иreturn
ценность.
После завершения выполненияasync2
удаляется из стека вызовов.
В этот момент,await
заблокируетasync2
Возвращаемое значение , выпрыгнуть первымasync1
Выполнить вниз.
Следует отметить, что сейчасasync1
серединаres
переменная илиundefined
, без задания.
с последующей казньюnew Promise
.
воплощать в жизньconsole.log("i")
.
В настоящее время,async1
Все внешние задачи синхронизации завершены, поэтому он возвращается в ранее заблокированное положение и выполняет его вниз.
В настоящее времяres
Назначено успешноasync2
результирующее значение , а затем выполнитеconsole.log("b")
.
В этот моментasync1
Это конец казни, и тогда она называетсяthen()
Функции помещаются в очередь микрозадач.
В настоящее времяscript
Все макрозадачи выполнены, и мы готовы очистить очередь микрозадач.
Первая очередь микрозадач, которая должна быть выполнена:promise then
, то есть выполнитconsole.log("h")
утверждение.
законченныйPromise then
После микрозадачи сразу начинается выполнениеasync1
изpromise then
микрозадачи.
В это время очередь микрозадач очистилась, и будет выполняться следующая макрозадача.
рендеринг страницы
Наконец, обновите и отобразите страницу в цикле событий, который такжеVue
Логика асинхронного обновления в .
Каждый раз, когда цикл событий заканчивается, то есть после выполнения макрозадачи и очистки очереди микрозадач, браузер будет выполнять обновление и рендеринг страницы.
Обычно частота обновления страницы нашего браузера составляет 60 кадров в секунду, что означает, что нам нужно обновлять каждые 16,67 мс, поэтому мы также стараемся обеспечить контроль цикла событий в пределах 16,67 мс, что является одной из причин, по которой нам необходимо оптимизировать производительность кода. .
Далее рассмотрим случай.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Event Loop</title>
</head>
<body>
<div id="demo"></div>
<script src="./src/render1.js"></script>
<script src="./src/render2.js"></script>
</body>
</html>
// render1
const demoEl = document.getElementById('demo');
console.log('a');
setTimeout(() => {
alert('渲染完成!')
console.log('b');
},0)
new Promise(resolve => {
console.log('c');
resolve()
}).then(() => {
console.log('d');
alert('开始渲染!')
})
console.log('e');
demoEl.innerText = 'Hello World!';
// render2
console.log('f');
demoEl.innerText = 'Hi World!';
alert('第二次渲染!');
в соответствии сHTML
порядок исполнения, исполняемый первымJavaScript
кодrender1.js
, поэтому интерпретатор помещает его в очередь макрозадач и начинает выполнение.
Первым, кого казнят,console.log("a")
.
С последующимsetTimeout
и добавьте его обратный вызов в очередь задач макроса.
Сразу после казниnew Promise
.
Так же поставьthen()
Вставьте в очередь микрозадач.
Сразу после казниconsole.log("e")
.
Наконец, измените текстовое содержимое узла DOM, но в это время страница не будет обновляться и отображаться.
В этот моментscript
Макрозадача также выполняется.
Затем начните очищать очередь микрозадач и выполнитеPromise then
.
В этот момент,alert
Уведомление, и после завершения этого оператора очередь микрозадач очищается, представляя конец первого цикла событий, и страница вот-вот будет отображена.
при нажатии закрытьalert
После этого цикл событий завершается и начинается отрисовка страницы.
После завершения рендеринга выполняется следующая задача макроса, а именноsetTimeout callback
.
Сразу после казниconsole.log("b")
.
В это время очередь задач макроса была очищена, ноhtml
Файл еще не был выполнен, поэтому введитеrender2.js
Вперед, продолжать.
сначала выполнитьconsole.log('f')
.
Затем снова измените текстовую информацию узла, и рендеринг страницы в это время все еще не будет обновлен.
Затем выполнитеalert
выписка при закрытииalert
После уведомления макрозадача завершается, а очередь микрозадач пуста, поэтому цикл событий также завершается, и в это время начинается второе обновление страницы.
Но если всеJavaScript
Если в коде используется встроенный метод, браузер сначала поместит дваscript
Кидайте их в очередь задач макроса, так порядок выполнения тоже будет другой, поэтому я не буду здесь их по одному выводить.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Event Loop</title>
</head>
<body>
<div id="demo"></div>
<script>
const demoEl = document.getElementById('demo');
console.log('a');
setTimeout(() => {
alert('渲染完成!')
console.log('b');
},0)
new Promise(resolve => {
console.log('c');
resolve()
}).then(() => {
console.log('d');
alert('开始渲染!')
})
console.log('e');
demoEl.innerText = 'Hello World!';
</script>
<script>
console.log('f');
demoEl.innerText = 'Hi World!';
alert('第二次渲染!');
</script>
</body>
</html>
输出:a c e d "开始渲染!" f "第二次渲染!" "渲染完成!" b