Сделайте несколько анимаций и узнайте о EventLoop

JavaScript
Сделайте несколько анимаций и узнайте о EventLoop

недавно учусь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(){}Контекст выполнения помещается в стек вызовов.

call_stack_1.gif

Затем начните выполнять функциюc, первая выполняемая инструкцияconsole.log('c').

Поэтому интерпретатор также помещает его в стек вызовов.

call_stack_2.gif

когдаconsole.log('c')После выполнения метода консоль выводит'c', стек вызовов удалит его.

call_stack_3.gif

затем выполнитьa()функция.

переводчик будетfunction a() {}Контекст выполнения помещается в стек вызовов.

call_stack_4.gif

выполнить немедленноa()Предложение в -console.log('a').

call_stack_5.gif

когда функцияaПосле выполнения стек вызовов удаляет контекст выполнения.

а затем выполнитьc()Остальные операторы функции, то есть выполнениеb()функция, поэтому ее контекст выполнения добавляется в стек вызовов.

call_stack_6.gif

выполнить немедленноb()Предложение в -console.log('b').

call_stack_7.gif

b()После выполнения стек вызовов удалит его.

В настоящее времяc()Выполнение также завершено, и стек вызовов также удален из стека.

call_stack_8.gif

На этом наше заявление заканчивается.

очередь задач

Вышеупомянутый случай кратко представляет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)После завершения выполнения, после очистки задачи синхронизации, она входит в стек вызовов для выполнения ответного действия.

async.png

Макрозадачи и микрозадачи

Говоря о синхронных задачах и асинхронных задачах ранее, я упомянулочередь задач.

В очереди задач она фактически делится наОчередь макросовиОчередь микрозадач, соответствующий хранящийся внутризадача макросаимикрозадачи.

Во-первых,И макрозадачи, и микрозадачи являются асинхронными задачами.

Разница между макрозадачами и микрозадачами заключается в порядке их выполнения, поэтому различают макрозадачи и микрозадачи.

В синхронных задачах выполнение задач выполняется в порядке кода, а выполнение асинхронных задач также должно выполняться в порядке.Атрибуты очереди: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

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

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

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

  1. Из очереди задач макроса следуйтеПоставить в очередь заказ, найти первую макрозадачу для выполнения, поместить ее в стек вызовов и начать выполнение;
  2. законченныйЗадача макросаПосле загрузки всех задач синхронизации, то есть после очистки стека вызовов, макрозадача выталкивается из очереди макрозадач, а затем очередь микрозадач начинает выполнять микрозадачи последовательно в соответствии с порядком входа.пока очередь микрозадач не опустеет;
  3. Когда очередь микрозадач очищается, цикл событий завершается;
  4. Затем в очереди задач макроса найдите следующую задачу макроса для выполнения и запустите второй цикл обработки событий, пока очередь задач макроса не будет очищена.

Вот несколько основных моментов:

  • Когда мы выполним его в первый раз, интерпретатор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помещается в очередь задач макроса и начинает выполнение задачи макроса.

task_queque_1.gif

В порядке кода выполнить сначалаconsole.log("a").

Контекст функции помещается в стек вызовов, и после выполнения стек вызовов удаляется.

task_queque_2.gif

выполнить следующийsetTimeout(), контекст функции также входит в стек вызовов.

task_queque_3.gif

так какsetTimeoutэто задача макроса, так что сделайте этоcallbackФункция помещается в очередь задач макроса, затем функция удаляется из стека вызовов, и выполнение продолжается вниз.

task_queque_4.gif

с последующимPromiseоператора, сначала поместите его в стек вызовов, а затем выполните его вниз.

task_queque_5.gif

воплощать в жизньconsole.log("c")иresolve(), тут особо нечего сказать.

task_queque_6.gif

Потомnew Promise().then()метод, это микрозадача, поэтому поместите ее в очередь микрозадач.

task_queque_7.gif

В настоящее времяnew PromiseКогда оператор завершает выполнение, он удаляется из стека вызовов.

Затем выполните выполнениеconsole.log('f').

task_queque_8.gif

В этот момент,scriptВыполнение задачи макроса завершено, поэтому она выталкивается из очереди задач макроса.

Затем начните очищать очередь микрозадач. Первое, что нужно сделать, этоPromise then, поэтому он помещается в стек вызовов.

task_queque_9.gif

а затем начать выполнениеconsole.log("d").

task_queque_10.gif

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

В это время первыйthen()Когда функция завершает выполнение, стек вызовов и очередь микрозадач удаляются.

task_queque_11.gif

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

Процесс выполнения аналогичен предыдущему, поэтому много говорить не буду.

task_queque_12.gif

В этот момент очередь микрозадач была очищена, и первый цикл обработки событий завершился.

Далее выполняется следующая макрозадача, т.е.setTimeout callback.

task_queque_13.gif

После выполнения он также удаляется из очереди задач макроса и стека вызовов.

В этот момент в очереди микрозадач нет задач, поэтому второй цикл обработки событий также завершается.

Задача макроса также очищается, поэтому выполнение этого кода завершено.

task_queque_14.gif

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").

async_await_1.gif

с последующимsetTimeout, поместите его обратный вызов в задачу макроса и продолжите выполнение.

async_await_2.gif

с последующим вызовомasync1()function, тем самым помещая контекст своей функции в стек вызовов.

async_await_3.gif

затем начните выполнениеasync1серединаconsole.log("a").

async_await_4.gif

Далееawaitутверждение ключевого слова.

awaitБолее поздний звонокasync2функцию, поэтому мы помещаем ее в стек вызовов.

async_await_5.gif

затем начните выполнениеasync2серединаconsole.log("c")returnценность.

После завершения выполненияasync2удаляется из стека вызовов.

async_await_6.gif

В этот момент,awaitзаблокируетasync2Возвращаемое значение , выпрыгнуть первымasync1Выполнить вниз.

Следует отметить, что сейчасasync1серединаresпеременная илиundefined, без задания.

async_await_7.gif

с последующей казньюnew Promise.

async_await_8.gif

воплощать в жизньconsole.log("i").

async_await_9.gif

В настоящее время,async1Все внешние задачи синхронизации завершены, поэтому он возвращается в ранее заблокированное положение и выполняет его вниз.

async_await_10.gif

В настоящее времяresНазначено успешноasync2результирующее значение , а затем выполнитеconsole.log("b").

async_await_11.gif

В этот моментasync1Это конец казни, и тогда она называетсяthen()Функции помещаются в очередь микрозадач.

async_await_12.gif

В настоящее времяscriptВсе макрозадачи выполнены, и мы готовы очистить очередь микрозадач.

Первая очередь микрозадач, которая должна быть выполнена:promise then, то есть выполнитconsole.log("h")утверждение.

async_await_13.gif

законченныйPromise thenПосле микрозадачи сразу начинается выполнениеasync1изpromise thenмикрозадачи.

async_await_14.gif

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

async_await_15.gif

рендеринг страницы

Наконец, обновите и отобразите страницу в цикле событий, который также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, поэтому интерпретатор помещает его в очередь макрозадач и начинает выполнение.

render_1.gif

Первым, кого казнят,console.log("a").

render_2.gif

С последующимsetTimeoutи добавьте его обратный вызов в очередь задач макроса.

render_3.gif

Сразу после казниnew Promise.

render_4.gif

Так же поставьthen()Вставьте в очередь микрозадач.

render_5.gif

Сразу после казниconsole.log("e").

render_6.gif

Наконец, измените текстовое содержимое узла DOM, но в это время страница не будет обновляться и отображаться.

В этот моментscriptМакрозадача также выполняется.

render_7.gif

Затем начните очищать очередь микрозадач и выполнитеPromise then.

render_8.gif

В этот момент,alertУведомление, и после завершения этого оператора очередь микрозадач очищается, представляя конец первого цикла событий, и страница вот-вот будет отображена.

render_9.gif

при нажатии закрытьalertПосле этого цикл событий завершается и начинается отрисовка страницы.

render_10.gif

После завершения рендеринга выполняется следующая задача макроса, а именноsetTimeout callback.

render_11.gif

Сразу после казниconsole.log("b").

render_12.gif

В это время очередь задач макроса была очищена, ноhtmlФайл еще не был выполнен, поэтому введитеrender2.jsВперед, продолжать.

render_13.gif

сначала выполнитьconsole.log('f').

render_14.gif

Затем снова измените текстовую информацию узла, и рендеринг страницы в это время все еще не будет обновлен.

Затем выполнитеalertвыписка при закрытииalertПосле уведомления макрозадача завершается, а очередь микрозадач пуста, поэтому цикл событий также завершается, и в это время начинается второе обновление страницы.

render_15.gif

Но если все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