Здравствуйте, в этой статье описан механизм работы Angular 2. Сегодня мы заглянем под капот популярного фреймворка.
Эта статья представляет собой вольный перевод отчета Тобиаса Боша — Компилятор Angular 2. Ссылку на оригинальный отчет вы найдете в конце статьи.
Обзор
Тобиас Бош — сотрудник Google и член команды разработчиков Angular, создавший большинство компонентов компилятора.Сегодня он рассказывает о том, как Angular работает изнутри.
И это не что-то невероятно сложное.
Я надеюсь, что вы используете что-то из того, что узнали, в своих приложениях.
Возможно, вы, как и наша команда, создадите свой собственный компилятор или небольшой фреймворк.
Я расскажу о том, как работает компилятор и как мы добились своей цели.
Что такое компилятор?
Это все, что происходит между вводом приложения (это могут быть ваши команды, шаблоны и т. д.) и работающим приложением.
Все, что происходит между ними, является прерогативой компилятора.
На первый взгляд может показаться, что это чистая магия.
Но это неправда.
Что мы посмотрим сегодня?
Сначала мы поговорим немного о производительности: что вы подразумеваете под быстрым бегом? Мы поговорим о том, какие типы входов у нас есть.
Мы также поговорим о лексическом анализе, что это такое и что делает с анализом компилятор Angular 2. Мы поговорим о том, как Angular 2 принимает разобранные данные и обрабатывает их.
В целом реализация этого процесса происходила в три попытки: сначала была простая реализация, затем улучшенная и еще более улучшенная.
Вот как Мы это сделали.
Это шаги, благодаря которым нам удалось добиться ускорения работы нашего компилятора.
Мы также обсудим различные среды, преимущества и недостатки динамической (Just In Time) и статической (Ahead Of Time) компиляции.
Производительность: что значит работать быстро?
Представим ситуацию: я написал приложение и утверждаю, что оно работает быстро.Что я имею в виду? Первое, о чем вы можете подумать, это то, что приложение загружается очень быстро.
Возможно, вы видели AMP-страницы Google? Они загружаются невероятно быстро.
Это можно отнести к понятию «быстро».
Возможно, я использую большое приложение.
Я переключаюсь из одного раздела в другой.
Например, от краткого обзора к подробной странице, и такой переход происходит очень быстро.
Это также может характеризовать скорость.
Или, допустим, у меня есть большая таблица, и теперь я просто собираюсь изменить все ее значения.
Я создаю новые ценности, не меняя структуру.
Это очень быстро.
Это все разные аспекты одной и той же концепции, разные вещи, на которые следует обратить внимание в приложении.
Переход по путям
Хотелось бы подробнее остановиться на этапе переключения маршрута.Важным моментом является то, что при движении по путям фреймворк разрушает и воссоздает все заново.
Оно не придерживается структуры: все разрушается и все воссоздается.
Как можно ускорить этот процесс? Чтобы сделать что-то быстро, нужно измерить эту скорость.
Для этого вам понадобится тест. Одним из тестов производительности, который мы используем, является тест производительности глубокого дерева.
Возможно, вы слышали об этом.
Это компонент, который используется дважды.
Это рекурсия компонента до достижения определенной глубины.
У нас есть 512 компонентов и кнопок, которые могут эти компоненты уничтожать или создавать.
Далее мы измеряем, сколько времени потребуется на разрушение и создание компонентов.
Так происходит переход по путям.
Переход от одного взгляда к другому.
Разрушение всего – это создание всего.
Какие входы у нас есть? Компоненты
У нас есть компоненты, и я думаю, что все их знают.
У них есть шаблон.
Вы можете сделать шаблоны встроенными или разместить их в отдельном файле, стоит помнить, что у них есть контекст. Экземпляры компонентов — это, по сути, контекст, данные, которые используются для создания шаблона.
В нашем случае у нас есть пользователь, у пользователя есть имя (в нашем случае это мое имя).
Образец Далее у нас есть шаблон.
Это простой HTML, сюда можно вставить что-то вроде формы ввода, можно использовать все, что предлагает HTML.
Здесь у нас есть новый синтаксис: помните двойные фигурные скобки, квадратные и круглые скобки? Это привязка Angular к свойствам или событиям.
Сегодня мы поговорим только о фигурных скобках и о том, что они означают. Семантически это означает «взять данные из компонента и поместить их в определенное место».
При изменении данных текст должен обновляться.
Директивы
Директивы содержат селектор — это селектор CSS. Дело в том, что когда Angular просматривает разметку и находит директиву, соответствующую какому-то элементу, он выполняет ее.
Допустим, у нас есть селектор форм, и мы говорим: каждый раз, когда мы создаем элемент формы, создайте эту директиву.
То же самое и с ngModel. Когда вы создаете атрибут ngModel, вы также должны создать директиву.
Эти директивы могут иметь зависимости.
Это наше выражение зависимости.
Зависимость имеет иерархическую структуру, поэтому ngModel запрашивает ngForm. Так что же делает Angular?
Он сканирует все дерево в поисках ближайшего ngForm, который находится на один уровень выше в древовидной структуре.
Он не будет смотреть на родственные элементы, а только на родительские.
Есть и другие входные данные, но мы не будем вдаваться в подробности.
Все, что делается в Angular, проходит через компилятор.
Хорошо, у нас есть входные данные.
Следующее, что нам нужно, это понять их.
Это просто чушь или в этом еще есть какой-то смысл?
Процесс лексического анализа
Начальная ступень
Допустим, у нас есть какой-то шаблон, например HTML. Представление шаблонаКак мы можем представить это так, чтобы компилятор это понял? Это делает анализатор.
Он считывает каждый символ, а затем анализирует его значение.
То есть он создает дерево.
Каждый элемент имеет только один объект. Допустим, есть какое-то имя — имя элемента, и есть дочерние элементы.
Скажем так, текстовый узел — это объект JSON с текстовыми свойствами.
У нас также есть атрибуты элементов.
Скажем так, мы закодируем все это как вложенные списки.
Первое значение — это ключ, второе — значение атрибута.
И так далее, здесь нет ничего сложного.
Такое представление называется абстрактным синтаксическим деревом (АСТ, англ.
– AST).
Вы часто будете слышать эту концепцию.
Это все HTML. Как мы можем изобразить связь элемента с данными?
Мы можем отобразить это вот так.
Это текстовый узел, то есть у нас есть JSON-объект с текстовыми характеристиками.
Текст пуст, поскольку изначально нет текста для отображения.
Текст зависит от входящих данных.
Входные данные представлены в этом выражении.
Любые выражения также анализируются, чтобы понять, что они означают. Вы не можете объявить функцию внутри выражения или использовать цикл for, но у нас есть такие вещи, как каналы, которые позволяют работать с выражениями.
Мы можем представить это выражение user.name как путь к свойству.
А еще мы можем понять, откуда именно взялось это выражение из вашего шаблона.
Определение места выражения Так почему же это так важно? Почему нам важно знать, откуда именно взялось это выражение из ПДД? Это потому, что мы хотим показывать вам сообщения об ошибках во время работы программы.
Скажем так, чтобы ваш пользователь знал об этих ошибках.
И потом, есть ли исключения? Например, невозможно прочитать имя из неопределенного.
Если такое произошло, то нужно зайти в отладчик ошибок и проверить, поставить точку останова на первой ошибке.
Тогда вам следует понять, где именно произошла ошибка.
Компилятор Angular предоставляет вам дополнительную информацию
Он показывает, откуда именно в шаблоне «растут» ножки этой ошибки.
Цель — показать вам, что ошибка возникает, например, из-за этой конкретной интерполяции во второй строке, 14-м столбце вашего шаблона.
Для этого нам нужно, чтобы номера строк и столбцов находились в ASD. Далее, какие анализаторы нам нужны для построения этого АСД? Здесь есть много возможностей.
Например, мы можем использовать браузер.
Браузер — отличный парсер HTML, верно? Он делает это каждый день.
У нас был такой подход при разработке Angular 1, и мы начали использовать тот же подход при разработке Angular 2. В настоящее время мы не используем браузер для таких целей по двум причинам:
- Вы не можете извлечь номера строк и столбцов из браузера.
При разборе HTML браузер их просто не использует.
- мы хотим, чтобы Angular работал и на сервере.
Мы могли бы сказать: в браузере мы используем браузер, а в сервере мы используем что-то другое.
Так оно и было.
Но затем мы зашли в тупик с такими вещами, как SVG и размещение запятых, поэтому нам нужна была одна и та же семантика повсюду.
Поэтому проще вставить фрагмент JavaScript и анализатор.
Это именно то, что мы делаем.
Итак, мы поговорили об HTML и выражениях.
Как мы представляем найденные директивы? Мы можем визуализировать их через объекты JSON, которые представляют элементы, просто добавив еще одно свойство: директивы.
И мы ссылаемся на функции-конструкторы этих директив.
В нашем примере ввода ngModel мы можем визуализировать этот элемент как объект JSON. Он имеет вход имени, атрибуты, ngModel и директивы.
У нас есть указатель на конструкторы, а также мы улавливаем зависимости, потому что нам нужно указать, что если мы создаем ngModel, нам также понадобится ngForm, и нам нужно получить эту информацию.
Учитывая ADS с HTML-информацией, соединениями и директивами, как нам все это реализовать? Какой самый простой способ сделать это? Давайте сначала разберемся с HTML. Какой самый простой способ создать элемент DOM? Во-первых, вы можете использовать внутренний HTML. Во-вторых, вы можете взять уже существующий элемент и клонировать его.
И в-третьих, вы можете вызвать document.createElement. Давайте проголосуем.
Кто думает, что метод InnerHTML самый быстрый? Кто думает, что element.cloneNode создаст элемент быстрее всех? Или, может быть, самый быстрый способ — element.createElement? Очевидно, что со временем все меняется.
Но сейчас:
- innerHTML — самый медленный вариант. Это очевидно, потому что браузер должен вызвать свой парсер, взять вашу строку, просмотреть каждый символ и построить элемент DOM. Очевидно, это очень медленно.
- element.cloneNode — самый быстрый способ, поскольку в браузере уже есть построенная проекция, и он просто клонирует ее.
Это просто добавление еще одного элемента в память.
Вот и все.
- document.createElement — это нечто среднее между двумя предыдущими методами.
Этот метод очень близок к element.cloneNode. Немного медленнее, но очень близко.
Так работал Angular 1, и так мы начинали разработку Angular 2. И как обычно оказывается, что это не совсем честное сравнение, по крайней мере, не в случае с Angular. В случае использования Angular нам нужно создать несколько элементов, но помимо этого нам нужно разместить эти элементы.
В нашем случае мы хотим создать новые текстовые узлы, но нам также нужно найти именно тот, который отвечает за user.name, так как мы хотим обновить его позже.
Поэтому, если сравнивать, то надо сравнивать и создание, и размещение.
Если вы используете innerHTML или cloneNode, вам придется пройти весь путь построения DOM заново.
Используя createElement или createTextNode, вы обходите эти шаги.
Вы просто вызываете метод и сразу получаете его выполнение.
Никаких новостроек и прочего.
В этом плане, если сравнивать createElement и createTextNode, они оба примерно одинаковы по скорости (в зависимости от количества привязок).
Во-вторых, требуется гораздо меньше структур данных.
Вам не нужно отслеживать все эти индексы и прочее, поэтому эти методы проще и почти одинаковы по скорости.
Поэтому мы используем эти методы, и другие фреймворки тоже переходят на этот подход. Итак, мы уже можем создавать DOM-элементы.
Теперь нам нужно создать директивы Нам нужна инъекция зависимостей от дочернего элемента к родительскому.
Допустим, у нас есть структура данных под названием ngElement, которая включает элемент DOM и директивы для этого элемента.
Также есть родительский элемент. Это простое дерево внутри дерева DOM.
И как мы можем создавать элементы DOM из ASD?
У нас есть шаблон, есть элемент, из которого мы построили АСД.
Что мы можем со всем этим сделать?
В нашем ngElement и конструкторе мы вызываем document.createElement, просматриваем атрибуты и присваиваем их элементам, а затем добавляем элемент к нашему родительскому элементу.
Как видите, никакой магии нет. Далее переходим к директивам.
Как это работает? Просматриваем привязки, каким-то образом получаем их (об этом я расскажу чуть позже) и просто снова вызываем new в конструкторе, передаем ему привязки и сохраняем Map. Карта перейдет от типа директив (ngModel) к экземплярам директив.
И весь этот поиск директив будет работать так: у нас будет метод, получающий директиву, который сначала проверяет сам элемент (если у него есть директива).
Если нет, то возвращаемся к родителю и проверяем там.
Это самое простое, что вы можете сделать.
Мы сами начали в этом направлении.
И это работает. Важная деталь: крепления.
Как отобразить привязки? Вы просто создаете класс привязки, у которого есть цель — Node. Это будет текстовый узел.
У цели есть свойство, в нашем случае это будет значение узла, это место, куда будет помещено значение.
И выражение.
Связывание работает следующим образом: каждый раз, когда вы оцениваете выражение или когда оно просто изменяется, вы сохраняете его в target.
То есть у вас может быть следующий метод: сначала вы оцениваете выражение, если оно изменилось, затем обновляете цель и другие ранее сохраненные значения.
Что касается исключений, обсуждавшихся ранее, мы вызываем методы try catch для отслеживания пути оценки.
Когда генерируется исключение, мы выбрасываем его повторно и создаем для него модель, используя номера строк и столбцов.
Таким образом мы получаем количество строк и столбцов, в которых есть ошибки.
Все это соединяем в презентацию.
Это последняя структура данных.
Представление — это элемент шаблона.
То есть, когда мы посмотрим на код ошибки, мы увидим множество представлений.
Это всего лишь элементы шаблона.
И объединяем их в презентацию.
Представление ссылается на компонент, элементы ng и привязки, а также на метод грязной проверки, который просматривает привязки и проверяет их.
Сейчас мы завершили первый этап.
У нас новый компилятор.
Насколько мы быстры? Почти на том же уровне, что и Angular 1. Неплохо.
Используя более простой подход, мы добились той же скорости.
Но Angular 1 не медленный.
Вторая стадия
Как мы можем ускорить этот процесс? Каким будет следующий шаг? Что мы упустили? Давайте разберемся.
Нам нужно что-то, что связано с нашими структурами данных.
Когда дело доходит до структуры данных, это на самом деле очень сложная проблема.
Если сравнить с предыдущей написанной нами программой, где появляется try-catch, но если отбросить это, то мы увидим, что многие функции замедляют процесс, и что многие моменты необходимо оптимизировать.
Если вы считаете, что структуры данных являются причиной медленной работы вашей программы, то это очень сложный вопрос, поскольку они разбросаны по всей вашей программе.
Это всего лишь предположение, что проблема в структурах данных.
Мы провели эксперименты и попытались это выяснить.
Мы наблюдали эти директивы: Map внутри ngElements.
Получается, что для каждого элемента DOM-дерева мы создаём новую карту? Можно сказать, что никаких директив там нет, не мы их создавали.
Но все равно мы всегда создаем карту, наполняем ее директивами и считываем с нее информацию.
Это может быть расточительно, может перегрузить память, а чтение всё равно отнимет некоторое время.
Альтернативный подход — сказать: «Хорошо, мы разрешаем только 10 директив на элемент. Далее мы создаем класс inlinengElement, 10 свойств для элементов директивы и типов директив, и чтобы найти директиву, мы создаем 10 условных операторов IF».
Это быстрее? Может быть.
Он не потребляет много ресурсов памяти, верно? Например, настройка: вы устанавливаете свойство, а не карту.
Чтение может быть немного медленным из-за 10 условий.
Это именно тот случай, для которого была оптимизирована виртуальная машина JavaScript. Виртуальная машина JavaScript может создавать скрытые классы (можете погуглить на досуге).
Это то, что делает виртуальную машину JavaScript быстрее.
Переход на эту структуру данных ускоряет процессы.
Результаты тестов производительности мы посмотрим позже.
Еще одна вещь, для которой вам необходимо оптимизировать структуры данных, — это повторное использование существующих экземпляров.
Можно задать логичный вопрос: если одни строки уничтожаются, а другие восстанавливаются, то почему бы не кэшировать эти строки в кэше и не менять данные, как только строки появляются? Вот что мы сделали.
Мы создали нечто, называемое кэшем представлений, который восстанавливает старые экземпляры представлений.
Прежде чем перейти в новый пул, состояние необходимо уничтожить.
Состояние содержится в директиве.
Таким образом мы уничтожим все директивы.
Далее при выходе из пула нужно заново создать эти директивы.
Именно это и делают методы гидратации и дегидратации.
Мы сохранили узлы DOM, потому что все исходит из модели, весь статус находится в модели.
Вот почему мы сохранили его.
Мы еще раз провели тест производительности.
Тестовая среда Чтобы дать вам представление о результатах этих тестов, стоит отметить, что Baseline — это жестко запрограммированная программа JavaScript, написанная вручную.
В такой программе не использовались никакие фреймворки; программа была написана только для этого теста глубокого дерева.
Программа выполняет грязную проверку.
Мы взяли эту программу за единое целое.
Все остальное сравнивается в соотношении.
Angular 1 получил оценку 5,7.
Ранее мы показывали такую же скорость с оптимизированными структурами данных и без кэша представлений.
У нас было 2,7. Так что это хороший показатель.
Мы удвоили скорость благодаря быстрому доступу к свойствам.
Сначала мы думали, что на этом наша работа закончилась.
Недостатки второго этапа На основе этого мы создали приложения.
Но потом мы увидели недостатки:
- У ViewCache не все в порядке с памятью.
Представьте, что вы переключаете маршруты обработки запросов.
Ваши старые запросы остаются в памяти, потому что они кэшируются, верно? Вопрос в том, когда удалять запросы из кеша? На самом деле это очень сложный вопрос.
Можно было бы создать несколько простых элементов, позволяющих пользователю выбирать, кэшировать или нет что-либо.
Но это было бы по меньшей мере странно.
- Еще одна проблема: элементы DOM имеют скрытое состояние.
Например, элемент имеет фокус.
Даже если у вас нет привязки фокуса и элемент может находиться в фокусе или нет, его удаление или возврат может изменить фокус того или иного элемента.
Мы не думали об этом.
Появились связанные с этим ошибки.
Один из способов, которым мы могли бы пойти, — это полностью удалять элементы, чтобы удалить их состояние, и даже восстановить их.
Но это свело бы на нет производительность ViewCache, если бы нам пришлось воссоздавать DOM. Ведь наш показатель был 2,7. Как бы нам добиться скорости в такой ситуации?
Третий этап
представление класса Тогда нам пришла мысль: давайте ещё раз взглянем на наш класс представления.Что у нас там? У нас есть компонент — это уже оптимизированный элемент, верно? У нас есть привязки.
Но класс представления по-прежнему содержит эти массивы.
Можем ли мы создать InlineView, который также использует только свойства? Никаких массивов.
Это возможно? Оказалось, да.
Как это выглядит? Как это.
Образец
Итак, шаблон у нас будет как и раньше, и для каждого элемента мы будем просто создавать код. Для этого шаблона мы будем создавать код, который отображает наш класс представления.
В конструкторе для каждого элемента мы будем вызывать document.createElement, который будет храниться в свойстве Node0 — для первого элемента, для второго мы снова будем вызывать document.createElement, который будет храниться в Node1. Далее, когда нам нужно прикрепить элемент к его родителю, у нас есть свойства, верно? Нам просто нужно сделать все в правильном порядке.
Мы можем использовать свойство для ссылки на предыдущее состояние.
Именно это мы и сделаем с DOM.
Директивы
То же самое мы делаем с директивами.
У нас есть свойства для каждого экземпляра.
И опять же, нам просто нужно убедиться, что порядок действий правильный: сначала идут зависимости, а потом компоненты, которые эти зависимости используют. Сначала мы используем ngForm, а затем ngModel. Привязки Далее крепления.
Мы просто создаем код, который выполняет грязную проверку.
Берем наши выражения, преобразуем их обратно в JavaScript. В данном случае это будет этот.компонент user.name. Это значит, что мы вытаскиваем user.name из компонента, сравниваем его с предыдущим значением, которое тоже является свойством.
Если значение изменилось, мы обновляем текстовый узел.
В конце концов мы сводим все к представлению со структурой данных.
Он содержит только свойства.
Массивов нет, везде используется Map, быстрый доступ через свойства.
Это значительно ускоряет процесс.
Скоро я покажу вам цифры, чтобы вы сами в этом убедились.
Вопрос в том, как нам это сделать? Допустим, кому-то нужно создать строку, которая оценивает этот новый класс.
Как это сделано? Мы просто применяем то, что мы сделали в реализации 101, к нашей текущей реализации.
Суть в следующем: если раньше мы создавали узлы DOM, то теперь мы создаём код для создания узлов DOM. Если раньше мы сравнивали элементы, то теперь создаём код для сравнения элементов.
Наконец, хотя раньше мы имели доступ к экземплярам директив или узлам DOM, теперь мы храним свойства, в которых хранятся экземпляры директив и узлы DOM. В данном случае код выглядит следующим образом.
Раньше у нас был ngelement, теперь — compileElement. Фактически эти классы теперь существуют в компиляторе.
Есть compileElement, compileView и так далее.
Отображение будет таким: раньше у нас был элемент DOM, теперь у нас есть только свойство, хранящее элемент DOM. Раньше мы вызывали document.createElement, теперь мы создаем строку с этой новой интерполяцией строк, которая отлично подходит для создания кода, в котором мы говорим, что this.document + имя его свойства эквивалентно document.createElement с именем ASD. Наконец, там, где мы ранее вызывали метод AppendChild, теперь мы создаем код для добавления дочернего элемента к родительскому элементу.
То же самое происходит и с поиском директив зависимостей.
Все происходит по тому же алгоритму, только теперь мы создаем код для этих целей.
Если мы сейчас посмотрим на показатели, то увидим, что мы теперь сильно увеличили скорость.
Если раньше наш показатель был 2,7, то теперь 1,5. Это почти в два раза быстрее.
ViewCache по-прежнему немного быстрее.
Но мы исключили вариант его использования, и причины нашего решения вы уже знаете.
Мы проделали отличную работу и могли бы уже закончить.
Но нет. Динамическая (точно в срок) компиляция Итак, вначале мы говорили о динамической (Just in Time) компиляции.
Динамический — это значит, что мы компилируем в браузере.
Напомним, это работает примерно так: у вас есть входящие данные, которые находятся на вашем сервере.
Браузер их скачивает и подхватывает, всё анализирует, создаёт оригинальный класс представления.
Теперь нам нужно оценить этот исходный код, чтобы получить другой класс.
После этого мы можем создать этот класс и тогда получим работающее приложение.
С этой частью есть определенные проблемы:
- Первая проблема заключается в том, что нам нужно анализировать и генерировать код внутри браузера.
Это занимает некоторое время, в зависимости от того, сколько компонентов у нас есть.
Нам необходимо пройти через все символы.
Конечно, это происходит довольно быстро.
И все же это тоже требует времени.
Поэтому ваша страница не будет загружаться быстрее всего.
- Вторая проблема — анализатор должен быть в браузере, поэтому вам нужно загрузить в браузер весь компилятор Angular. Следовательно, чем больше свойств мы добавляем и чем больше кода создаем, тем больше становится размер.
- Следующая проблема – использование
-
Знаменитости И Игры-Одевалки
19 Oct, 24 -
Научим Машину Понимать Человеческие Гены
19 Oct, 24 -
Популярный Мотиватор На Вашей Работе
19 Oct, 24 -
Кисмет
19 Oct, 24