Посмотрите наш видеокурс:

Https://www.udemy.com/course/javascript-boost/?referralCode=06AF8499F008CC241201

Базовые знания структуры данных для разработчиков JavaScript

Структура данных - это наука о хранении и управлении данными.

В этом уроке мы изучим три фундаментальных концепции структуры данных: очередь, стек и куча.

Эти три концепции являются метафорами для описания трех различных способов хранения и управления данными и задачами в памяти.

Очередь следует принципу FIFO. FIFO означает «первым пришел - первым ушел».

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

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

Добавление новой задачи в очередь называется постановкой в ​​очередь. Удаление задачи из очереди называется dequeue.

В очереди будут храниться два указателя: передний указатель и задний указатель.

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

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

Со стеком все работает в обратном направлении. Он следует принципу LIFO: первым пришел - ушел. Первая задача в стеке будет выполняться последней, а последняя задача, входящая в стек, будет выполнена первой. Мы используем термин фрейм для обозначения задач стека.

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

Добавление новой задачи в стек называется push. Перемещение задачи из стека называется всплыванием. Нижняя часть стека фиксирована, поэтому для управления им нужен только один указатель. Мы называем этот указатель вершиной, и он указывает на последний элемент, добавленный в стек. Другими словами, задача, указанная верхним указателем, будет выполнена первой.

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

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

Отсутствие порядка дает кучу большую гибкость с точки зрения хранения данных. Мы используем кучу для хранения объектов, поскольку их размеры часто непредсказуемы. В стеке мы можем получить доступ к объектам через их адрес в памяти.

Как работают стек, очередь и куча, на самом деле довольно сложная тема. Откровенно говоря, как JS-разработчику нет необходимости глубоко копать в этой области.

В языках более низкого уровня, таких как C, вам нужно управлять пространством памяти вручную, включая выделение пространства памяти с помощью malloc и освобождение пространства с помощью free.

Но в JS вам не нужно делать это самостоятельно. Движок JS сделает это за вас.

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

Процесс и поток

JavaScript - это однопоточный язык. Так что это значит?

Нить и процесс часто говорят вместе.

Процесс означает выполнение всей программы.

Поток означает сущность внутри процесса.

Один процесс состоит из нескольких потоков.

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

Однопоточность означает, что JavaScript может обрабатывать только одну задачу за раз. Это ограничивает вычислительную мощность, но положительным моментом является то, что разработка программы относительно проще, поскольку нам не нужно беспокоиться о координации различных потоков.

Чтобы повысить производительность, JavaScript имитирует или моделирует поведение многопоточного языка. Это достигается путем разделения всей программы на синхронный код и асинхронный код.

Синхронные коды будут выполняться по порядку. После выполнения первой задачи запустится вторая.

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

Синхронные коды также блокируются, то есть текущая задача блокирует задачу, определенную после нее.

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

Если мы поместим метод предупреждения между первым и вторым оператором журнала консоли, второй и третий оператор журнала консоли не будут выполняться, пока мы не нажмем кнопку ОК во всплывающем окне. Другими словами, метод предупреждения заблокирует выполнение второго и третьего операторов журнала консоли.

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

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

Обратный вызов в функции setTimeOut является асинхронным. Коды, определенные в нем, будут сначала пропущены и выполнены позже, когда все синхронные, определенные за ним, будут выполнены.

Давайте переместим метод предупреждения в обратный вызов функции setTimeOut.

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

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

Это потому, что обратный вызов функции setTimeOut является асинхронным. Механизм JS пропустит обратный вызов, а не функцию setTimeOut.

Обратите внимание: асинхронным является обратный вызов, а не функция setTimeOut.

Если мы запишем в консоль возвращаемое значение функции setTimeOut, вы обнаружите, что оно будет выведено без каких-либо задержек.

В вашей реальной разработке наиболее часто используемые примеры async - это обещание и ajax.

Создание конструктора обещания ничем не отличается от создания обычного конструктора. Это синхронно. Мы сразу получим объект обещания. Обещание может быть отложено, разрешено или отклонено, в зависимости от того, когда вызывается функция resolve () или reject ().

Асинхронные методы then, catch и finally.

По той же причине Promise.resolve () и Promise.reject () работают синхронно. Они немедленно передадут нам выполненный и отклоненный объект обещания.

Присоединенные к ним методы then, catch и finally являются асинхронными.

Отправка запроса в серверную программу с использованием ajax является синхронной.

Ожидание ответа асинхронное.

Если вы используете выборку, сам метод выборки будет синхронным. Два метода then, которые мы привязали к нему, являются асинхронными.

Стек и переполнение стека

В этом уроке мы рассмотрим механизм выполнения JavaScript.

JavaScript - это однопоточный язык. Он может работать только с одной задачей за раз. Одна задача может включать в себя один или несколько шагов.

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

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

Так как же с этим справляется JS-движок?

Как мы уже говорили ранее, JS-движок выполняет коды с использованием стека.

Функция будет помещена в стек первой. Тело его функции будет выполняться как единое целое. Таким образом, три оператора журнала консоли, определенные в нем, будут выполняться в том же порядке, в котором они определены.

НО…

Что, если внутри функции мы вызвали другую функцию?

В этом случае внутренняя функция также будет помещена в стек.

Обратите внимание: к тому моменту, когда внутренняя функция помещается в стек, выполнение внешней функции еще не завершено. Функция будет извлечена из стека только после того, как будет выполнено все ее тело функции. В результате на данный момент у нас в стеке есть две функции.

Размер стопки не бесконечен. Если мы добавим в стек слишком много задач, система выйдет из строя. Это называется переполнение стека.

Самый простой способ создать переполнение стека - вызвать функцию изнутри, не задавая никаких условий остановки.

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

Ниже приведен правильный пример использования рекурсии.

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

Цикл событий, макрос и микрозадача

Движок JS пропустит асинхронные коды и выполнит их позже. Итак, как движок JS управляет этими пропущенными асинхронными кодами? Как убедиться, что он не забудет определенные пропущенные коды? Если у нас есть несколько пропущенных кодов, как JS-движок контролирует порядок выполнения?

Прежде чем мы сможем ответить на эти вопросы, нам необходимо изучить две новые концепции: макрос задача и микро задача.

Чтобы лучше управлять асинхронными кодами, JavaScript разделяет асинхронные коды на макро-задачу и микро-задачу. Макрозадача часто для краткости называется задачей.

Итак, как мы узнаем, какая задача асинхронной обработки является макросом, а какая - микро?

Метод then, catch и finally - это микрозадачи, потому что они не являются независимой операцией. Они являются вторым этапом создания объекта обещания.

Обратные вызовы setTimeOut и setImmediate являются макро-задачами, потому что они независимы и, что наиболее важно, так сказали авторы JS.

Когда JS-движок впервые запускает программу, она находится в глобальном контексте.

Когда JS-движок впервые запускает программу, она находится в глобальном контексте.

Он найдет все синхронные коды в глобальной области видимости и выполнит их по порядку.

Что касается асинхронных, то они будут добавлены в очередь.

Макро-задачи будут добавлены в очередь макросов, а микро-задачи - в микро-очередь.

После того, как все синхронные задачи будут выполнены, JS-движок проверит очередь микро-задач и выполнит хранящиеся в ней задачи.

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

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

Вот вам вызов: попробуйте предсказать результат следующей программы.

new Promise(resolve =>{
  resolve(1);
  Promise.resolve().then(() => console.log(2));
  console.log(4)
})
.then(t => console.log(t));
console.log(3);

Ответ: 4321.

Еще более сложная задача для вас: что, если в методе then есть функция setTimeOut? Это означает, что у нас есть макро-задача, вложенная в микро-задачу. Попробуйте предугадать результат следующей программы.

console.log(1)
setTimeout(()=>{
  console.log(2)
},100)
new Promise((resolve,reject)=>{
  console.log(3)
  setTimeout(()=>{
    resolve(4)
    console.log(5)
  },0)
})
.then((val)=>{
  console.log(val)
})
setTimeout(()=>{
  console.log(6)
},0)

Ответ: 135642

process.nextTick ()

В среде nodeJS process.nextTick () также рассматривается как микрозадача. Но он особенный. Лично я не думаю, что nextTick следует рассматривать как микрозадачу, по крайней мере, не обычную. Он должен принадлежать к своей уникальной категории. Мы разберемся с этим в следующем уроке.

Он будет выполняться после завершения всех синхронных задач и до того, как будут выполнены задачи из очереди микрозадач.

Попробуйте предугадать результат следующей программы:

setTimeout(()=>{
  console.log(1)
},10)
new Promise((resolve,reject)=>{
  resolve(2)
})
.then((val)=>{
  console.log(val)
})
process.nextTick(()=>{
  console.log(3)
})
console.log(4)

Ответ: 4321.