ES модулі: детальний схематичний огляд

19 хв. читання

ES модулі привносять у JavaScript офіційну, стандартизовану модульну систему. Знадобився час, щоб до цього дійти — майже 10 років роботи по стандартизації.

Але очікування майже закінчилось. З випуском Firefox 60 (на даний час у стані бета-версії) у травні всі основні браузери підтримуватимуть ES модулі, а робоча група модулів Node зараз працює над підтримкою модулів ES у Node.js. Інтеграція модулів ES у WebAssembly теж в процесі.

Багато JavaScript розробників знають, що ES модулі є суперечливими, але мало хто насправді розуміє, як вони працюють.

Які проблеми вирішують модулі?

Якщо подумати, все програмування на JavaScript — просто керування змінними. Присвоєння значень змінним або додавання чисел до змінних, або об'єднання двох змінних разом і об'єднання їх в іншу змінну.

ES модулі

Оскільки так багато вашого коду стосується лише зміни змінних, тоді те, як ви їх організуєте, сильно впливатиме на те, наскільки добре ви можете програмувати... і на те, наскільки добре ви можете підтримувати цей код.

Коли ви маєте всього декілька змінних, про які слід думати одночасно, — все просто. В JavaScript є спосіб допомогти вам це зробити, — область видимості. Через те, як працюють області видимості в JavaScript, функції не можуть отримати доступ до змінних, визначених в інших функціях.

ES модулі

Це добре. Оскільки це означає, що коли ви працюєте над однією функцією, то можете думати лише про цю функцію. Вам не потрібно піклуватися про те, що інші функції можуть зробити з вашими змінними.

Тим не менш, є також і недолік: це ускладнює обмін змінними між різними функціями.

Що робити, якщо ви хочете поділитися своєю змінною поза межами області видимості функції? Розмістити змінну в області видимості рівнем вище... наприклад, зробити змінну глобальною.

Вірогідно, ви це пам'ятаєте з часів jQuery. Перш, ніж завантажувати будь-які плагіни jQuery, вам потрібно впевнитись, що він знаходиться в глобальній області видимості.

ES модулі

Працює, але випливає кілька дратівливих проблем.

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

Якщо ви зіпсуєте цей порядок, тоді ваш застосунок під час виконання видасть помилку. Коли функція шукатиме jQuery там, де очікує його знайти — в глобальній області видимості — але не знайде його, вона видасть помилку й завершить виконання.

ES модулі

Це ускладнює підтримку коду, і робить видалення старого коду чи тегів скриптів грою в рулетку — ви не знаєте, що може зламатися. Залежності між цими різними частинами вашого коду неявні. Будь-яка функція може взяти що завгодно з глобальної області видимості тому ви не знаєте, які функції від яких скриптів залежать.

Друга проблема полягає в тому, що, оскільки ці змінні в глобальній області видимості, кожна частина коду всередині цієї області може змінити змінну. Шкідливий код може змінити її з ціллю змусити код робити щось вами незаплановане, а не шкідливий — випадково її зламати.

Як модулі допомагають?

Модулі дають кращий спосіб організації цих змінних та функцій. За допомогою модулів ви групуєте змінні та функції, які має сенс групувати разом.

Це ставить ці функції та змінні в область видимості модуля. Вона може використовуватися для обміну змінними між функціями у модулі.

Але на відміну від областей видимості функцій, області видимості модулів мають спосіб робити свої змінні доступними також і для інших модулів. Вони можуть явно вказати, яка зі змінних, класів або функцій у модулі має бути доступна.

Коли щось робиться доступним для інших модулів, це називається експортом. Після експорту модулі можуть явно вказати, що вони залежать від цієї змінної, класу або функції.

ES модулі

Оскільки це явне відношення, ви можете сказати, які модулі зламаються, якщо ви видалите інший.

Як тільки у вас є можливість експортувати та імпортувати змінні між модулями, це значно полегшить розбиття вашого коду на невеликі частини, які можуть працювати незалежно одна від іншої. Потім ви можете комбінувати та рекомбінувати ці частини, на кшталт кубиків Lego, для створення всіх різноманітних типів застосунків з одного й того самого набору модулів.

Через те, що модулі такі корисні, було декілька спроб додати модульну функціональність у JavaScript. Сьогодні активно використовуються дві модульні системи. CommonJS (CJS) — те, що Node.js історично використовував. ESM (модулі EcmaScript) — новіша система, яка була додана у специфікацію JavaScript. Браузери вже підтримують ES модулі, а Node додає підтримку.

Розгляньмо детально як працює ця нова модульна система.

Як працюють ES модулі

При розробці з допомогою модулів ви створюєте граф залежностей. З'єднання між різними залежностями беруться з будь-яких імпортованих операторів, які ви використовуєте.

Імпортовані оператори — те, як браузер або Node точно знає, який код потрібно завантажити. Ви даєте йому файл для використання в ролі точки входу в граф. З цього моменту він просто слідує за будь-яким з імпортованих операторів, щоб знайти решту коду.

ES модулі

Але самі файли — не те, що може використовувати браузер. Йому потрібно проаналізувати їх всі, щоб перетворити їх в структури даних, які називаються модульними записами (Module Records). Таким чином, браузер дійсно знатиме, що відбувається у файлі.

ES модулі

Після цього запис модуля має бути перетворений в екземпляр модуля. Екземпляр об'єднує дві речі: код та стан.

Код, в основному, являє собою набір інструкцій. Це схоже на рецепт з виготовлення чогось. Але сам по собі код ви не можете ні для чого використовувати. Вам потрібні вихідні матеріали для використання з цими інструкціями.

Що таке стан? Стан дає вам ці вихідні матеріали. Він — актуальні значення змінних в будь-який момент часу. Звичайно, ці змінні — лише нікнейми для комірок пам'яті, які містять значення.

Отже, екземпляр модуля об'єднує код (список інструкцій) зі станом (всі значення змінних).

ES модулі

Нам потрібен екземпляр модуля для кожного модуля. Процес завантаження модуля переходить з цього файлу вхідної точки до повного графа екземплярів модулів.

Для модулів ES це відбувається за три кроки:

  1. Конструювання — знайти, завантажити та проаналізувати всі файли в модульних записах.
  2. Створення екземплярів — знайти комірки в пам'яті, щоб помістити в них всі експортовані значення (але поки що не заповнювати їх значеннями). Потім зробити, щоб експорт й імпорт вказували на ці комірки в пам'яті. Це називається зв'язкою.
  3. Обчислення — запустити код, щоб заповнити комірки поточними значеннями змінних.

ES модулі

Люди говорять про те, що ES модулі є асинхронними. Ви можете про це думати як про асинхронність, тому що робота ділиться на три різних фази — завантаження, створення екземплярів та обчислення — і ці фази можуть виконуватися окремо.

Це означає, що специфікація вводить певну асинхронність, якої не було в CommonJS. Я пізніше поясню більше, але у CJS модуль та залежності завантажуються, утворюють екземпляри та оцінюються одразу, без будь-яких перерв між ними.

Однак, самі кроки необов'язково асинхронні. Вони можуть бути виконані синхронно. Це залежить від того, що робить завантаження. Це через те, що не все контролюється специфікацією модулів ES. Насправді, є дві половини роботи, які покриваються різними специфікаціями.

Специфікація модулів ES говорить, як ви повинні розбирати файли у модульні записи, і як ви маєте створювати екземпляри й обчислювати цей модуль. Однак, вона не говорить, як отримати файли в першу чергу.

Це завантажувач витягує файли. І він вказується в іншій специфікації. Для браузерів ця специфікація є специфікацією HTML. Але ви можете мати різні завантажувачі, базуючись на тому, яку платформу використовуєте.

ES модулі

Завантажувач також точно контролює як завантажуються модулі. Він викликає методи модулів ES —  ParseModule, Module.Instantiate та Module.Evaluate.

ES модулі

Роздивімося тепер кожен окремий крок більш докладно.

Конструювання

Під час цього етапу з кожним модулем відбуваються три речі:

  1. Визначити звідки завантажити файл, який містить модуль (роздільна здатність модуля)
  2. Витягти файл (завантаживши його з URL або з файлової системи)
  3. Розібрати файл у модульний запис.

Пошук файлу та його вилучення

Завантажувач потурбується про пошук файлу та його завантаження. Спершу йому потрібно знайти файл точки входу. В HTML ви вказуєте завантажувачу, де його шукати, за допомогою використання тегу скрипта:

ES модулі

Але як він знайде наступну групу модулів — від яких безпосередньо залежить main.js?

В цьому випадку вводяться оператори імпорту. Одна частина оператору називається специфікатором модуля. Вона говорить завантажувачу, де він може знайти наступний модуль.

ES модулі

Одне зауваження щодо специфікаторів модулів: іноді їх потрібно по-різному обробляти між браузером і Node. Кожен хост має свій власний спосіб інтерпретації рядків специфікатора модуля. Для цього він використовує щось, що називається алгоритмом розподільної здатності модулів, які розрізняються між платформами. На даний час деякі специфікатори модулів, які працюють у Node, не працюватимуть у браузері, але зараз це намагаються виправити.

Поки це не виправлено, браузери, як специфікатори модулів, приймають лише URL-адреси. Вони будуть завантажувати модульний файл з цієї адреси. Але це не відбувається для всього графу одночасно. Ви не знаєте, які залежності потребує модуль для вилучення, поки не проаналізуєте файл... і ви не зможете проаналізувати файл, поки ви його не вилучите.

Це означає, що ми повинні проходити через дерево пошарово, проаналізувати один файл, визначити його залежності, а потім знайти й завантажити ці залежності.

ES модулі

Якщо основний потік чекав завантаження кожного з цих файлів, в черзі залишалось багато інших задач.

Це через те, що при роботі в браузері, завантажувана частина займає багато часу.

ES модулі

Блокування основного потоку як цей, зробило б застосунок, який використовує модулі, занадто повільним для використання. Це одна з причин того, чому специфікація модулів ES розбиває алгоритм на декілька фаз. Виділення для конструкції своєї власної фази дозволяє браузерам витягати файли й формувати розуміння модульного графа, перш ніж перейти до синхронної роботи створення екземплярів.

Цей підхід — алгоритм, розділений на фази — одна з ключових відмінностей між модулями ES та CommonJS.

CommonJS може робити все по-іншому, оскільки завантаження файлів з файлової системи займає набагато менше часу, ніж завантаження через інтернет. Це означає, що Node може блокувати основний потік під час завантаження файлу. І оскільки файл вже завантажений, має сенс просто утворювати екземпляри та обчислювати (що у CommonJS не є окремими фазами). Це також означає, що ви спускаєтесь по всьому дереву, завантажуєте, створюєте екземпляри та обчислюєте будь-які залежності, перш ніж повертати екземпляр модуля.

ES модулі

Підхід CommonJS має декілька наслідків, і я пізніше розкажу про них детальніше. Але одна річ, яка має значення — це те, що у Node з модулями CommonJS ви можете використовувати змінні у своєму специфікаторі модуля. Ви виконуєте весь код у цьому модулі (до оператора require), перш ніж шукати інший модуль. Це значить, що змінна матиме значення при переході до розподільної здатності модуля.

Але з модулями ES ви заздалегідь створюєте цей граф модулів... перш, ніж проводите будь-які обчислення. Це означає, що у вас не може бути змінних у специфікаторах модулів, тому що ці змінні поки що не мають значень.

ES модулі

Але іноді дійсно корисно використовувати змінні для шляхів модулів. Наприклад, вам, можливо, закортить перемкнути те, який модуль завантажувати в залежності від того, що робить код або в якому середовищі він працює.

Щоб зробити це можливим для модулів ES, існує пропозиція, яка називається динамічне імпортування. Завдяки цьому ви можете використовувати оператор імпорту, наприклад, import('${path}/foo.js').

Спосіб роботи полягає в тому, що будь-який файл, завантажений за допомогою використання import(), обробляється як точка входу до окремого графу. Динамічно імпортований модуль починає новий граф, який обробляється окремо.

ES модулі

Однак, слід зазначити, що будь-який модуль, який знаходиться на обох цих графах, буде ділитися екземпляром модуля. Це пов'язане з тим, що завантажувач кешує екземпляри модулів. Для кожного модуля в певній глобальній області видимості буде тільки один екземпляр модуля.

А це означає менше роботи для рушія. Наприклад, файл модуля буде вилучатися тільки один раз, навіть якщо від нього залежать кілька модулів. (Це одна з причин кешування модулів. Іншу ми побачимо в розділі, присвяченому обчисленню.)

Завантажувач керує цим кешем, використовуючи карту модулів. Кожна глобальна область видимості відстежує свої модулі на окремій карті модулів.

Коли завантажувач витягує адресу, він поміщає її в карту модулів і робить помітку про те, що зараз він витягає файл. Потім він надішле запит і перейде до вилучення наступного файлу.

ES модулі

Що станеться, якщо інший модуль залежить від того самого файлу? Завантажувач шукатиме кожну адресу в карті модулів. Якщо він в ній знайде fetching, то просто перейде до наступної адреси.

Але карта модулів не просто відстежує, які файли витягаються. Вона також слугує в ролі кешу для модулів, що ми й побачимо далі.

Розбір

Тепер, коли ми вилучили файл, нам потрібно розібрати його у модульний запис. Це допоможе браузеру зрозуміти, що являють собою різні частини модуля.

ES модулі

Як тільки модульний запис створюється, він поміщається у карту модулів. Це означає, що з цього моменту кожного разу, коли він запитується, завантажувач зможе витягти його з цієї карти.

ES модулі

Але у Node ви не використовуєте HTML теги, так що у вас немає можливості використати атрибут type. Одним зі способів, яким спільнота намагалася виправити це — використовувати розширення .mjs. Використання цього розширення повідомляє Node, що цей файл є модулем.

В будь-якому випадку, завантажувач визначить, чи слід розбирати файл як модуль, чи ні. Якщо це модуль і в ньому є імпортовані файли, тоді він знову запустить процес, поки всі файли не будуть вилучені та розібрані.

І це все! В кінці процесу завантаження з лише одного файлу точки входу ви отримуєте купу модульних записів.

ES модулі

Наступний крок — створити екземпляри цього модуля й об'єднати їх всі разом.

Створення екземплярів

Як вже було сказано раніше, екземпляр об'єднує код зі станом. Стан знаходиться в пам'яті, тому крок створення екземплярів повністю присвячений підключенню до пам'яті.

По-перше, рушій JS створює запис середовища модуля, який керує змінними для модульних записів. Потім він шукає комірки в пам'яті для всього експорту. Запис середовища модуля відстежуватиме, яка комірка в пам'яті пов'язана з кожним експортом.

Ці комірки в пам'яті ще не отримають значення. Їх фактичні значення будуть заповнені тільки після обчислення. До цього правила є одне застереження: будь-які експортовані оголошення функцій ініціалізуються на цьому етапі. Це дозволяє спростити обчислення.

Щоб створити екземпляр графа модулей, рушій робитиме так званий наскрізний обхід першого порядку глибини. Це означає, що він спуститься до нижньої частини графа — до залежностей, які ні від чого не залежать — і налаштує їх експорти.

ES модулі

Рушій завершує підключення всіх експортів нижче модуля — всі експорти, від яких залежить модуль. Тоді він повертається на рівень вище, щоб під'єднати імпорти з цього модуля.

Зверніть увагу, що експорт й імпорт вказують на одне й те саме місце в пам'яті. Підключення експортів першим чином гарантує те, що всі імпорти можуть бути підключені до відповідного експорту.

ES модулі

У цьому полягає різниця від модулів CommonJS. У CommonJS весь експортований об'єкт копіюється при експорті. Це означає, що будь-які експортовані значення (як наприклад, числа) є копіями.

Якщо експортований модуль змінить це значення пізніше, то імпортований модуль не побачить цю зміну.

ES модулі

На противагу, ES модулі використовують так звані «живі прив'язки». Обидва модулі вказують на те саме місце в пам'яті. Це означає, що коли експортований модуль змінює значення, то ця зміна відобразиться в імпортованому модулі.

Модулі, які експортують значення, можуть змінювати ці значення в будь-який час, але імпортовані модулі не можуть змінювати значення їх імпортів. При цьому, якщо модуль імпортує об'єкт, він може змінювати значення властивостей цього об'єкта.

ES модулі

Причиною мати живі прив'язки є те, що ви можете під'єднати всі модулі без запуску будь-якого коду. Це допоможе з обчисленням, коли у вас є циклічні залежності, про що я поясню нижче.

Отже, в кінці цього кроку ми маємо всі екземпляри та комірки пам'яті для підключених експортів/імпортів.

Тепер ми можемо розпочати обчислення й заповнювати ці комірки пам'яті значеннями.

Обчислення

Останній крок — заповнення цих комірок в пам'яті. Рушій JS робить це, виконуючи код верхнього рівня — код, який знаходиться поза функціями.

Окрім просто заповнення цих комірок, обчислення коду також може викликати побічні ефекти. Наприклад, модуль може викликати сервер.

ES модулі

Через потенційні побічні ефекти, ви тільки раз захочете обчислити модуль. На відміну від зв'язування, яке відбувається під час створення екземпляру, і може виконуватися кілька разів з точно тим самим результатом, обчислення може мати різні результати в залежності від того, скільки разів ви його виконуєте.

Це одна з причин мати карту модулів. Вона кешує модуль за канонічною адресою, так що для кожного модулю мається тільки один модульний запис. Це гарантує те, що кожний модуль виконується тільки один раз. Як і при створенні екземпляру, це робиться як наскрізний обхід першого порядку глибини.

Як щодо тих циклів, про які ми говорили раніше?

В циклічній залежності ви отримуєте цикл у графі. Зазвичай, це довгий цикл. Але щоб пояснити проблему, я збираюсь використати вигаданий приклад з коротким циклом.

ES модулі

Подивімося, як це працюватиме з модулями CommonJS. Спершу головний модуль виконав би запитуваний оператор. Тоді він завантажив би модуль counter.

ES модулі: детальний схематичний огляд

Модуль counter спробував би отримати доступ до message з експортованого об'єкта. Але оскільки message ще не було обчислено у головному модулі, повернеться undefined. Рушій JS буде виділяти простір в пам'яті для локальної змінної й встановить значення в undefined.

ES модулі

Обчислення продовжиться до кінця коду верхнього рівня модулю counter. Ми хочемо дізнатися, чи отримаємо ми в кінцевому підсумку правильне значення для message (після обчислення main.js), тому ми встановили тайм-аут. Тоді обчислення у main.js відновлюється.

ES модулі

Змінна message буде ініціалізована й додана у пам'ять. Але оскільки між ними нема ніякого зв'язку, вона залишиться undefined у запрошуваному модулі.

ES модулі

Якби експорт був оброблений, використовуючи живі прив'язки, модуль counter в кінцевому підсумку побачив би правильне значення. К часу спливання тайм-ауту, обчислення main.js завершилося б та заповнило значення.

Підтримка цих циклів є основною причиною розробки модулів ES. Саме цей трифазний дизайн робить їх можливими.

Який нині статус модулів ES?

З релізом Firefox 60 на початку травня всі основні браузери за замовчуванням підтримуватимуть ES модулі. Node також додає підтримку, за допомогою робочої групи, присвяченої виявленню проблем сумісності між модулями CommonJS та ES.

Це означає, що у вас буде можливість використовувати тег скрипта за допомогою type=module, та використовувати імпорти й експорти. Однак, ще більше можливостей модулів чекає попереду. Пропозиція динамічного імпорту на третій стадії в процесі розробки специфікації, як і import.meta, яка допоможе підтримувати варіанти використання Node.js, а пропозиція розподільної здатності модулів допоможе згладити розбіжності між браузерами та Node.js. Тому ви можете чекати на те, що робота з модулями в майбутньому стане ще краще.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.2K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація