Понимание цикла событий в последний раз

JavaScript

Цикл событий — это основная идея асинхронного программирования на 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.

Web 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

Графически понимать цикл событий движка JavaScript

Механизм потоков JavaScript и механизм событий