В этой статье мы сосредоточимся на том, как движок JavaScript выполняет асинхронную операцию. В моей предыдущей статье Как на самом деле работает код в Javascript? (Часть 1 — Синхронность движка JS), я объяснил, как он выполняет синхронные операции и каковы основные части движка JavaScript. Если вы пропустили часть 1, вы можете ознакомиться с ней здесь.

Как объяснялось в предыдущей части, JavaScript является однопоточным, и код выполняется синхронно, что означает, что каждая строка кода выполняется в порядке появления кода. Но если нам нужна такая задача, как доступ к внешнему источнику данных, и это занимает много времени? А давайте усложним, что, если нам нужно запустить код, который использует данные с внешнего сервера, и если у нас очень медленное интернет-соединение?

Если у вас нет никакого опыта работы с JavaScript и если вы только что прочитали первую часть, вы, вероятно, скажете, что «Надо ждать и мы ничего не можем сделать, пока данные не поступят из внешнего источника». Другими словами, он будет заблокирован. То, что я объясню ниже, спешит нам помочь в этот момент.

JavaScript недостаточно для таких задач. Итак, нам нужно добавить в головоломку новые кусочки (которые вовсе не являются JavaScript). Мы можем перечислить эти новые части как

  • API-интерфейсы веб-браузера / фоновые API-интерфейсы узла
  • Цикл событий, обратный вызов/очередь задач
  • Обещания

API веб-браузера/фоновые API узла

Как я упоминал в части 1, изначально JavaScript был разработан для запуска скриптов на веб-сайтах. А браузеры имеют множество доступных веб-API. Они не являются частью самого языка JavaScript, а построены поверх основного языка JavaScript, предоставляя вам дополнительные возможности для использования в вашем коде JavaScript.

Как упоминается в Mozilla Developer Network, API-интерфейсы браузера встроены в браузер и могут предоставлять данные из браузера и окружающей компьютерной среды, а также выполнять с ними полезные сложные действия. Например, Web Audio API предоставляет конструкции JavaScript для управления звуком в браузере — взятие звуковой дорожки, изменение ее громкости, применение к ней эффектов и т. д. В фоновом режиме браузер фактически использует некоторый сложный низкоуровневый код (например, , C++ или Rust) для фактической обработки звука. Но опять же, API абстрагирует эту сложность от вас.

Пока мы кодируем на JavaScript, мы сознательно или неосознанно используем многие API-интерфейсы браузера. Как разработчик JavaScript, я могу четко сказать, что API браузера, которые я использовал очень часто, — это «Timer (setTimeOut, setInterval), HTML DOM (document), Fetch API (fetch), API веб-хранилища (localStorage, sessionStorage, indexedDB) и т. д. ”

Я на 100% уверен, что эти API-интерфейсы браузера не являются чем-то странным для многих из нас, но основная цель этой статьи — объяснить, как JS Engine взаимодействует с этими API-интерфейсами. Позвольте мне объяснить это на примере.

Когда мы проверяли приведенный выше код, ожидалось, что мы должны увидеть «Hello» в первой строке, а затем мы должны увидеть «hi!» во второй строке консоли из-за синхронной структуры JavaScript. Но когда мы принимаем во внимание API-интерфейсы браузера, все становится сложнее. Давайте проверим схему ниже.

Как вы видите на схеме выше, из-за потока выполнения, в первой строке js-движок сохраняет функцию приветствия в память, а затем в строке 5 отправляет в браузер сообщение для API таймера с длительностью и команда, которая будет выполнена, когда продолжительность закончится. В этот момент в браузере запустится таймер. После этого шага JS-движок выполнит строку 7 и «привет!» будет видно на консоли. Когда заданная продолжительность, 1000 мс, будет завершена, функция в части таймера при завершении будет выполнена движком js, а сообщение «Hello» будет видно на консоли. Другими словами, несмотря на то, что функция setTimeout была вызвана ранее, вместо ожидания выполнения этой функции, console.log("hi!") в строке 7 будет выполнено немедленно и "hi!" сообщение будет видно сначала. Это распространенный пример асинхронного выполнения в JavaScript.

Очередь обратного вызова и цикл обработки событий

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

Как вы видите выше, в это время я добавил функцию veryLongIteration, которая будет блокировать стек вызовов в течение 1 секунды и временной аргумент функции setTimeout 0 мс. Позвольте мне объяснить, как будет выполняться этот блок кода. Поскольку мы сосредоточены на времени, я объясню, что происходит на временной шкале.

Перед выполнением: функцияgreet и функция veryLongIteration сохраняются в памяти.

0 мс: вызывается функцияsetTimeout, и веб-браузер запускается для таймера. Поскольку аргумент длительности таймера равен 0 мс, как только запускается браузер, задача в браузере завершается.

Прежде чем продолжить объяснение, позвольте мне кратко рассказать вам об очереди обратного вызова и цикле обработки событий. Вы можете себе представить, что Callback Queue — это очередь задач, в которой задачи ожидают выполнения перехода к стеку вызовов. Когда есть задача, которая поступает из браузера, и если глобальный контекст выполнения все еще включает некоторый код, ожидающий выполнения, или если стек вызовов в этот момент занят, эти задачи ожидают в очереди обратного вызова. С другой стороны, вы можете представить цикл событий как своего рода цикл, который проверяет глобальный контекст выполнения и стек вызовов, заняты они или нет, и всякий раз, когда доступны условия, он отправляет задачи из очереди обратного вызова в стек вызовов.

В свете информации об очереди обратного вызова и цикле событий, когда таймер в браузере завершается. Функция обратного вызова setTimeOut, Greet(), отправляется в очередь обратного вызова для ожидания отправки в стек вызовов.

1 мс: как объяснялось выше, поскольку глобальный контекст выполнения все еще включает некоторые коды, ожидающие выполнения, функция приветствия должна продолжать ожидать в очереди обратного вызова. И поток выполнения продолжится до следующей строки, включая вызов функции veryLongIteration. Предполагается, что функция veryLongIteration включает тяжелый итерационный процесс, который займет 1 секунду. Поскольку это синхронный процесс, во время его выполнения глобальный контекст выполнения будет заблокирован до завершения выполнения.

1001 мс: если вы ожидаете, что настала очередь функции приветствия, вы ошибаетесь. Я уже упоминал правило об очереди обратного вызова, согласно которому, если глобальный контекст выполнения включает какой-либо код, ожидающий выполнения, цикл обработки событий не будет отправлять никакую задачу из очереди в стек вызовов. Поэтому мы продолжаем со строки 13, и console.log("hi!") будет выполнен.

1002 мс:Наконец-то! В глобальном контексте выполнения нет кода, ожидающего выполнения, а стек вызовов пуст! Теперь цикл событий может переместить функцию приветствия из очереди обратного вызова в стек вызовов, и мы можем увидеть «привет!» сообщение в консоли.

Когда я впервые услышал эту структуру, я не мог поверить, что функция обратного вызова setTimeout выполняется с такой задержкой, и мне нужно было это проверить. Но JavaScript обеспечивает асинхронность с помощью Timer API именно в таком порядке. Чтобы схематически представить структуру, вы можете проверить изображение ниже.

Обещания

Способ обработки асинхронных задач, поставляемый с ECMAScript 6 (ES6), позже переименованный в ECMAScript 2015.

Согласно Mozilla Developer Network, «обещание — это прокси для значения, которое не обязательно известно при создании обещания. Он позволяет связать обработчики с возможным значением успеха или причиной отказа асинхронного действия. Это позволяет асинхронным методам возвращать значения так же, как и синхронным: вместо немедленного возврата окончательного значения асинхронный метод возвращает обещание предоставить значение в какой-то момент в будущем».

Чтобы лучше понять, могу привести пример. Представьте, что вы хотите остановиться в отеле на Рождество и знаете, что в это время года нет свободных номеров. Чтобы остановиться в этом отеле в это время, у вас есть два варианта. Первый бронирует номер раньше. И вам не нужно беспокоиться о своем месте. Ты уже гарантировал свою комнату. Второй просит пустой номер, когда вы приедете в отель и ждете пустой номер. Как вы уже догадались, если вы выберете второй вариант, вы будете заблокированы до тех пор, пока не получите номер. Промисы — это что-то вроде первого варианта, вы можете установить место для будущего значения переменной, состояния и т. д. в памяти, и пока это значение не будет предоставлено, вы можете продолжить следующую часть кода.

Позвольте мне привести вам еще один пример. Как я упоминал в первой части, чтобы сделать сетевой запрос, мы используем API веб-браузера, который является «выборкой». Для меня fetch — очень мощное ключевое слово, предоставляющее очень важную функцию для веб-разработки. С другой стороны, функция выборки имеет два фасада. Пока он запрашивает данные из внешнего источника, как только он вызывается, он возвращает объект в часть JavaScript. Помимо того, что он является обычным объектом JS, он имеет некоторые дополнительные свойства, которые являются скрытыми свойствами. Эти свойства относительно «onFullFilled» и «onRejection».

Прежде чем привести пример с фрагментом кода, позвольте мне объяснить две концепции промисов. Как я упоминал ранее, объект обещания имеет два скрытых свойства: onFullFilled и onRejection. Эти свойства предназначены для определения того, что происходит, когда выборка успешна, и что происходит, если выборка не удалась, соответственно. Вначале значения этих двух свойств представляют собой пустые массивы. Чтобы дать значение этим свойствам. Мы используем два метода: «then()» и «catch()».

Метод «.then()» может принимать два аргумента; первый аргумент — это функция обратного вызова для onfullfilled, что означает успешное выполнение обещания. Второй аргумент, который можно использовать вместо «.catch()», — это функция обратного вызова для onrejection. Каждый «.then()» возвращает вновь сгенерированный объект обещания, который можно дополнительно использовать для цепочки.

Позвольте мне объяснить объект обещания с помощью кода ниже.

Как видно из схемы выше, перед тем, как JS-движок начнет выполняться, он сохраняет в памяти функции приветствия и catchError. И, как вы видите в строке 9, справа от равенства вызывается функция выборки. Как уже было сказано в первой части, будет создан контекст выполнения.

Как только контекст выполнения будет создан, функция fetch создаст объект обещания с пустым onFullFilled и пустым onRejection. Затем, в строке 11, метод «then» и метод «catch» последовательно предоставляют значения для свойств onFullFilled и onRejection. начал. Это «два фасада» функции выборки.

Когда сетевой запрос успешно завершен в части браузера, данные, поступающие из внешнего источника, назначаются ключу «значение» объекта обещания, а функция приветствия, которая является свойством onFullFilled объекта обещания, будет перемещена в стек вызовов. и затем стек вызовов выполнит его. Если сетевой запрос завершится ошибкой, тот же процесс будет выполняться для свойства onRejection.

Подводя итог…

Хотя каждый пункт, описанный выше, содержит в себе очень подробные процессы, их можно обобщить примерно так. Хотя JS Engine изначально был разработан для синхронного выполнения задач, он стал более мощным и надежным благодаря этим асинхронным функциям. Поэтому популярность языка JavaScript растет день ото дня, и мы можем создавать не только веб-приложения или веб-сайты с помощью JavaScript, но и огромное количество различных продуктов.

Источники

Введение в веб-API — Изучите веб-разработку | МДН

Изучение сложных частей JS | Мастера интерфейса

Обещание — JavaScript | МДН

Цикл событий — JavaScript | МДН