Цикл событий — это основная идея асинхронного программирования на JavaScript, а также препятствие, которое необходимо преодолеть при продвижении внешнего интерфейса. В то же время, это также обязательный тестовый сайт для интервью, особенно после появления промисов, различные вопросы интервью появляются один за другим, с различными трюками. Эта статья начинается с примеров из реальной жизни, что позволит вам полностью понять принципы и механизмы Event Loop и с легкостью решать такие вопросы на собеседовании.
Городское здание письменных тестовых вопросов на гнилой улице во вселенной
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
Почему JavaScript однопоточный?
Все мы знаем, что JavaScript — это单线程
Язык, что означает, что вы можете делать только одну вещь за раз. Это связано с тем, что JavaScript родился как язык сценариев браузера, в основном используемый для взаимодействия с пользователем, работы в сети и управления DOM. Это определяет, что он может быть только однопоточным, иначе возникнут сложные проблемы с синхронизацией.
Предполагая, что в JavaScript есть два потока, один поток добавляет содержимое в узел DOM, а другой поток удаляет этот узел, какой поток должен выбрать браузер?
Поскольку Javascript является однопоточным, это похоже на банк с одним окном, и клиенты должны стоять в очереди для обработки один за другим. Точно так же задачи JavaScript должны выполняться одна за другой.Если определенная задача (например, загрузка изображений высокой четкости) требует много времени, почему браузер должен все время зависать? Чтобы предотвратить блокировку основного потока, JavaScript имеет同步
а также异步
Концепция чего-либо.
синхронный и асинхронный
Синхронизировать
Функция является синхронной, если вызывающая сторона может получить ожидаемый результат при возврате. Другими словами, после запуска синхронного вызова метода вызывающая сторона должна дождаться возврата вызова функции, прежде чем продолжить последующее поведение. Следующий фрагмент кода сначала вызовет всплывающее окно с предупреждением, если вы не нажмете确定
кнопку, все взаимодействие со страницей блокируется, а последующиеconsole
Заявления не печатаются.
alert('Yancey');
console.log('is');
console.log('the');
console.log('best');
асинхронный
Если вызывающая сторона не может получить ожидаемый результат при возврате функции, но должна получить его определенными средствами в будущем, то функция является асинхронной. Например, чтобы отправить сетевой запрос, мы говорим основной программе подождать, пока данные не будут получены, а затем уведомить меня, а затем мы можем делать другие вещи. Когда асинхронка будет завершена, нас уведомят, но в это время программа может заниматься другими делами, поэтому даже если асинхронка будет завершена, ей нужно подождать в стороне, а когда программа простаивает, будет время посмотреть который асинхронно был завершен, а затем выполнить .
Вот почему таймер не может выдать результат функции обратного вызова ровно через указанное время.
setTimeout(() => {
console.log('yancey');
}, 1000);
for (let i = 0; i < 100000000; i += 1) {
// todo
}
Стек выполнения и очередь задач
Давайте рассмотрим структуры данных
-
Стек: стек — это упорядоченная коллекция, которая следует принципу «последним пришел — первым вышел» (LIFO). Новые добавленные или удаленные элементы хранятся на одном конце, который называется вершиной стека, а другой конец называется низом. стека. В стеке новые элементы находятся в верхней части стека, а старые элементы — в нижней части стека. В стеке хранятся указатели, вызовы методов и т. д. примитивных типов данных и объектов в компиляторе и памяти языка программирования.
-
Очередь (queue): Очередь — это упорядоченная коллекция, работающая по принципу "первым пришел - первым ушел" (FIFO). Очередь добавляет новые элементы в конец и удаляет элементы вверху. Последний добавленный элемент должен быть в конце. очереди. В информатике наиболее распространенным примером являются очереди печати.
-
Куча: куча — это специальная структура данных, основанная на абстрактном типе данных дерева.
Как показано на рисунке выше, память в JavaScript делится на堆内存
а также栈内存
,
Размер значений ссылочного типа в JavaScript не фиксирован, поэтому они хранятся в堆内存
, пространство для хранения автоматически выделяется системой. JavaScript не допускает прямого доступа к местам в куче памяти, поэтому мы не можем напрямую манипулировать пространством кучи памяти объекта, а вместо этого манипулируем对象的引用
.
В то время как все основные типы данных в JavaScript имеют фиксированный размер, поэтому они хранятся в栈内存
середина. Мы можем напрямую манипулировать значениями, хранящимися в пространстве памяти стека, поэтому основные типы данных按值访问
. Кроме того, стековая память также хранит对象的引用 (指针)
так же как函数执行时的运行空间
.
Давайте сравним разницу между двумя методами хранения.
стек памяти | куча памяти |
---|---|
Сохраните базовый тип данных | хранить ссылочный тип данных |
доступ по значению | доступ по ссылке |
Сохраняемые значения имеют фиксированный размер | Сохраненное значение является неопределенным и может быть динамически изменено |
Автоматически выделять память системой | Назначается программистом через код |
В основном используется для выполнения программ | В основном используется для хранения объектов |
Небольшое пространство, высокая эффективность работы | Большое пространство, но относительно низкая эффективность работы |
ФИФО, ЛИФО | Неупорядоченное хранилище, можно получить напрямую по ссылке |
стек выполнения
Когда мы вызываем метод, JavaScript создает среду выполнения, соответствующую методу, также называемую контекстом выполнения. Эта среда выполнения сохраняет частную область действия метода, верхнюю область действия (цепочку областей действия), параметры метода, а также переменные, определенные в этой области видимости, и указатель на нее, а также при последовательном вызове ряда методов. . Поскольку JavaScript является однопоточным, эти методы размещаются по порядку в одном месте, называемом стеком выполнения.
очередь задач
Очередь событий — это хранилище异步任务
Задачи в очереди выполняются в строгом хронологическом порядке, задачи в голове очереди будут выполняться первыми, а задачи в конце очереди будут выполняться последними. Очередь событий выполняет только одну задачу за раз, и после выполнения задачи выполняется следующая задача. Стек выполнения — это работающий контейнер, аналогичный стеку вызовов функций. Когда стек выполнения пуст, механизм JS проверяет очередь событий. Если очередь событий не пуста, очередь событий помещает первую задачу в стек выполнения для запуска . . .
цикл событий
Мы заметили, что по-прежнему можно ждать на стороне после завершения асинхронного кода, потому что программа в это время может заниматься другими делами, и ожидание пока программа простаивает, успеет посмотреть, какой асинхронный код завершился. Таким образом, в JavaScript есть механизм для обработки синхронных и асинхронных операций, и это цикл событий.
Ниже приведена схема цикла событий.
На словах это выглядит примерно так:
-
Все задачи синхронизации выполняются в основном потоке, образуя стек контекста выполнения.
-
Асинхронная задача будет помещена в таблицу задач, которая является модулем асинхронной обработки на рисунке выше.Когда асинхронная задача имеет результат выполнения, функция будет перемещена в очередь задач.
-
Как только все задачи синхронизации в стеке выполнения будут выполнены, механизм прочитает очередь задач и поместит первую задачу в очередь задач в стек выполнения для запуска.
Основной поток продолжает повторять третий шаг, т.只要主线程空了,就会去读取任务队列
, процесс повторяется непрерывно, что называется事件循环
.
Макрозадачи и микрозадачи
Микрозадачи, макрозадачи и цикл событийВ этой статье используются интересные примеры для объяснения макро- и микрозадач. Скопируйте ее ниже.
Возьмем пример похода в банк по делам.После того, как кассир в окне № 5 обработал текущего клиента, он начинает звонить по номеру для приема следующего клиента.Мы сравниваем каждого клиента с宏任务
,接待下一位客户
Процесс заключается в том, чтобы позволить следующему宏任务
в стек выполнения.
Таким образом, все клиенты этого окна помещаются в任务队列
середина. Очередь задач已经完成的异步操作的
, вместо регистрации асинхронная задача будет помещена в эту очередь задач (она будет помещена в Таблицу задач). Точно так же, как при заказе номера в банке, если вас нет на месте, когда вам звонят, то ваш текущий номерной знак будет недействителен, и кассир решит пропустить бизнес-обработку следующего клиента, и вам нужно будет получить номер снова после того, как вы вернетесь.
При выполнении макрозадач можно чередовать некоторые микрозадачи. Например, после того, как ваш дядя закончил дело, он между прочим спросил у кассира: "Р2Р-гроза в последнее время была очень серьезной. Есть ли другой безопасный способ инвестировать?" Кассир был трезв: «Очередной дурак клюнул», а потом много бормотал.
Давайте проанализируем этот процесс.Хотя дядя закончил нормальные дела, он проконсультировался по поводу финансовой информации.В это время кассир не должен говорить: «Иди назад, чтобы взять номер и снова встать в очередь». Таким образом, пока кассир может справиться с этим, это будет сделано до ответа на следующую задачу макроса.Мы можем понимать эти задачи как微任务
.
Выслушав, дядя поднял улыбку на 45 градусов и сказал: «Я просто спрошу».
Кассир ОС: "Бля..."
Этот пример иллюстрирует:Твой дядя всегда будет твоим дядей 在当前微任务没有执行完成时,是不会执行下一个宏任务的!
Подводя итог, асинхронные задачи делятся на宏任务(macrotask)
а также微任务 (microtask)
. Макрозадачи помещаются в одну очередь, а микрозадачи — в другую очередь, и микрозадачи опережают выполнение макрозадач.
Общие макрозадачи и микрозадачи
Задачи макроса: скрипт (общий код), setTimeout, setInterval, ввод-вывод, события, postMessage, MessageChannel, setImmediate (Node.js)
Микрозадачи: Promise.then, MutaionObserver, process.nextTick (Node.js)
ответить на несколько вопросов
Посмотрите, сможете ли вы решить проблему ниже.
setTimeout(() => {
console.log('A');
}, 0);
var obj = {
func: function() {
setTimeout(function() {
console.log('B');
}, 0);
return new Promise(function(resolve) {
console.log('C');
resolve();
});
},
};
obj.func().then(function() {
console.log('D');
});
console.log('E');
-
Первый
setTimeout
Поместите его в очередь задач макроса, в это время очередь задач макроса ['A'] -
Затем выполните метод func объекта obj, который
setTimeout
Поместите его в очередь задач макроса, в это время очередь задач макроса ['A', 'B'] -
Функция возвращает обещание, потому что это синхронная операция, поэтому сначала распечатайте
'C'
-
Тогда будет
then
Поместите его в очередь микрозадач, в это время очередь микрозадач ['D'] -
Затем выполните задачу синхронизации
console.log('E');
,распечатка'E'
-
Поскольку микрозадачи выполняются первыми, сначала выводятся данные.
'D'
-
Последний выход
'A'
а также'B'
Давайте посмотрим на вопрос Учителя Жуань Ифэн, который на самом деле не сложный.
let p = new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => console.log(2));
console.log(4);
}).then(t => console.log(t));
console.log(3);
-
Первое место
Promise.resolve()
Метод then() помещается в очередь микрозадач, а очередь микрозадач имеет вид ['2']. -
Затем распечатайте задачу синхронизации
4
-
Тогда будет
p
Метод then() помещается в очередь микрозадач, в это время очередь микрозадач ['2', '1'] -
распечатать задачи синхронизации
3
-
Наконец, распечатайте микрозадачи последовательно
2
а также1
Когда цикл событий сталкивается с async/await
Мы знаем, что async/await — это просто синтаксический сахар для генераторов, так что не бойтесь, просто преобразуйте его в форму Promise. Следующий код представляет собой классическую форму функции async/await.
async function foo() {
// await 前面的代码
await bar();
// await 后面的代码
}
async function bar() {
// do something...
}
foo();
вawait 前面的代码
является синхронным и будет выполняться непосредственно при вызове этой функции; иawait bar();
Это предложение можно преобразовать вPromise.resolve(bar())
;await 后面的代码
Он будет помещен в метод then() объекта Promise. Таким образом, приведенный выше код можно преобразовать в следующую форму, это очень понятно?
function foo() {
// await 前面的代码
Promise.resolve(bar()).then(() => {
// await 后面的代码
});
}
function bar() {
// do something...
}
foo();
Возвращаясь к теме гнилой улицы в открывающемся космосе, давайте "рефакторим" код, а затем разбираем его, не так ли просто?
function async1() {
console.log('async1 start'); // 2
Promise.resolve(async2()).then(() => {
console.log('async1 end'); // 6
});
}
function async2() {
console.log('async2'); // 3
}
console.log('script start'); // 1
setTimeout(function() {
console.log('settimeout'); // 8
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1'); // 4
resolve();
}).then(function() {
console.log('promise2'); // 7
});
console.log('script end'); // 5
-
Первая распечатка
script start
-
Тогда будет
settimeout
Добавить в очередь задач макросов. В настоящее время очередь задач макросов['settimeout']
-
затем выполните функцию
async1
, сначала распечататьasync1 start
,и потому, чтоPromise.resolve(async2())
является синхронной задачей, поэтому распечатайтеasync2
, тоasync1 end
Добавить в очередь микрозадач, в настоящее время очередь микрозадач ['async1 end'] -
Затем распечатайте
promise1
,Будуpromise2
Добавить в очередь микрозадач, в это время очередь микрозадач['async1 end', promise2]
-
распечатка
script end
-
Поскольку приоритет микрозадачи выше, чем у макрозадачи, она распечатывается первой.
async1 end
а такжеpromise2
-
Наконец, распечатайте задачу макроса
settimeout
Споры на эту тему: Статья публикуется около двух дней, и один за другим поступают комментарии от друзей. в основном
async1 end
а такжеpromise2
проблема последовательности. я здесьChrome 73.0.3683.103 for MAC
а такжеNode.js v8.15.1
тестasync1 end
доpromise2
,существуетFireFox 66.0.3 for MAC
тестasync1 end
послеpromise2
.
Разница между Node.js и циклом событий в среде браузера
После обновления Node.js до версии 11.x принцип работы Event Loop изменился: после выполнения макрозадачи (setTimeout, setInterval и setImmediate) на этапе немедленно выполняется очередь микрозадач, что соответствует браузер. .
О разнице между Node.js и циклом обработки событий в среде браузера до версии 11.x см.@Волны в лодкебольшой парень«В чем разница между браузером и циклом событий Node?», тут не много глупостей.
Говоря о веб-воркерах
Следует подчеркнуть, что Worker является функцией браузера (то есть среды хостинга) и фактически не имеет почти никакого отношения к самому языку JavaScript. Тем не менее, JavaScript в настоящее время не поддерживает многопоточное выполнение.
Итак, JavaScript — это однопоточный язык! JavaScript — это однопоточный язык! JavaScript — это однопоточный язык!
Браузеры могут предоставлять несколькоJavaScript 引擎实例
, каждый из которых работает в своем собственном потоке, поэтому вы можете запускать разные программы в каждом потоке. Каждая такая независимая многопоточная часть программы называется Worker. Такой тип параллелизма называется任务并行
, потому что его основное внимание уделяется разделению программы на фрагменты для одновременного выполнения. Ниже приведена схема рабочего процесса Worker.
Экземпляр веб-воркера
Давайте поговорим об использовании Worker на примере факториала.
Сначала создайте новыйindex.html
, непосредственно в коде:
<body>
<fieldset>
<legend>计算阶乘</legend>
<input id="input" type="number" placeholder="请输入一个正整数" />
<button id="btn">计算</button>
<p>计算结果:<span id="result"></span></p>
</fieldset>
<legend></legend>
<script>
const input = document.getElementById('input');
const btn = document.getElementById('btn');
const result = document.getElementById('result');
btn.addEventListener('click', () => {
const worker = new Worker('./worker.js');
// 向 Worker 发送消息
worker.postMessage(input.value);
// 接收来自 Worker 的消息
worker.addEventListener('message', e => {
result.innerHTML = e.data;
// 使用完 Worker 后记得关闭
worker.terminate();
});
});
</script>
</body>
Создайте новый в том же каталогеwork.js
, содержание следующее:
function memorize(f) {
const cache = {};
return function() {
const key = Array.prototype.join.call(arguments, ',');
if (key in cache) {
return cache[key];
} else {
return (cache[key] = f.apply(this, arguments));
}
};
}
const factorial = memorize(n => {
return n <= 1 ? 1 : n * factorial(n - 1);
});
// 监听主线程发过来的消息
self.addEventListener(
'message',
function(e) {
// 响应主线程
self.postMessage(factorial(e.data));
},
false,
);
Закончите двумя вопросами
Следующие два вопроса исходят из@小美娜娜статьяEventloop — это не страшно, страшно встретить Promise. Скопируй, и ты меня не ударишь, а.
Первый вопрос
const p1 = new Promise((resolve, reject) => {
console.log('promise1');
resolve();
})
.then(() => {
console.log('then11');
new Promise((resolve, reject) => {
console.log('promise2');
resolve();
})
.then(() => {
console.log('then21');
})
.then(() => {
console.log('then23');
});
})
.then(() => {
console.log('then12');
});
const p2 = new Promise((resolve, reject) => {
console.log('promise3');
resolve();
}).then(() => {
console.log('then31');
});
-
Первая распечатка
promise1
-
Тогда будет
then11
,promise2
Добавить в очередь микрозадач, в это время очередь микрозадач['then11', 'promise2']
-
распечатка
promise3
,Будуthen31
Добавить в очередь микрозадач, в это время очередь микрозадач['then11', 'promise2', 'then31']
-
распечатка
then11
,promise2
,then31
, очередь микрозадач в это время пуста -
Буду
then21
а такжеthen12
Добавить в очередь микрозадач, в это время очередь микрозадач['then21', 'then12']
-
распечатка
then21
,then12
, очередь микрозадач в это время пуста -
Буду
then23
Добавить в очередь микрозадач, в это время очередь микрозадач['then23']
-
распечатка
then23
второй вопрос
На самом деле эта тема посвящена изучению использования промиса.Когда возвращает промис в методе1(), второе завершение P1 будет зависать при возврате этого промиса, поэтому порядок вывода следующий.
const p1 = new Promise((resolve, reject) => {
console.log('promise1'); // 1
resolve();
})
.then(() => {
console.log('then11'); // 2
return new Promise((resolve, reject) => {
console.log('promise2'); // 3
resolve();
})
.then(() => {
console.log('then21'); // 4
})
.then(() => {
console.log('then23'); // 5
});
})
.then(() => {
console.log('then12'); //6
});
наконец
Добро пожаловать в мой публичный аккаунт WeChat: начало атаки
Ссылаться на
JavaScript, которого вы не знаете (Том 2) - Кайл Симпсон
На этот раз досконально изучите механизм выполнения JavaScript.
Говоря о цикле событий JavaScript из темы
Микрозадачи, макрозадачи и цикл событий
Продвинутая основа интерфейса: подробная диаграмма пространства памяти JavaScript
Подробный Javascript в механизме цикла событий (контур событий)
Eventloop — это не страшно, страшно встретить Promise