[Перевод] Что происходит, когда JS-модуль импортируют дважды?

Начнём этот материал с вопроса. ES2015-модуль increment содержит следующий код:

// increment.js
let counter = 0;
counter++;

export default counter;


В другом модуле, который мы назовём consumer, вышеприведённый модуль импортируется 2 раза:

// consumer.js
import counter1 from './increment';
import counter2 from './increment';

counter1; // => ???
counter2; // => ???


А теперь, собственно, вопрос. Что попадёт в переменные counter1 и counter2 после выполнения модуля consumer?

-egq3px2qo7a_nyjchhabkyhms4.png

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

Выполнение модуля


Для того чтобы понять то, как работают внутренние механизмы JavaScript, полезно заглянуть в спецификацию.

В соответствии со спецификацией, каждый JavaScript-модуль ассоциирован с сущностью Module Record (запись модуля). У этой записи есть метод Evaluate(), который выполняет модуль:

Если данный модуль уже был успешно выполнен, вернуть undefined; […]. В противном случае транзитивно выполнить все зависимости этого модуля и затем выполнить этот модуль.
В результате оказывается, что один и тот же модуль выполняется лишь один раз.

К сожалению, то, что нужно знать для ответа на наш вопрос, этим не ограничивается. Как удостовериться в том, что вызов инструкции import с использованием одинаковых путей приведёт к возврату одного и того же модуля?

Разрешение команд импорта


За связь пути к модулю (спецификатора, specifier) с конкретным модулем отвечает абстрактная операция HostResolveImportedModule (). Код импорта модуля выглядит так:

import module from 'path';


Вот что говорит об этом спецификация:

Реализация HostResolveImportedModule должна соответствовать следующим требованиям:

  • Обычным возвращаемым значением должен быть экземпляр конкретного подкласса Module Record.
  • Если сущность Module Record, соответствующая паре referencingScriptOrModule, specifier, не существует, или такая сущность не может быть создана, должно быть выброшено исключение.
  • Каждый раз, когда эта операция вызывается с передачей ей в качестве аргументов конкретной пары referencingScriptOrModule, specifier, она должна, в случае её обычного выполнения, возвращать тот же самый экземпляр сущности Module Record.


Теперь рассмотрим это в более понятной форме.

HostResolveImportedModule(referencingScriptOrModule, specifier) — это абстрактная операция, которая возвращает модуль, соответствующий паре параметров referencingScriptOrModule, specifier:

  • Параметр referencingScriptOrModule — это текущий модуль, то есть — тот модуль, который выполняет импорт.
  • Параметр specifier — это строка, которая соответствует пути модуля в инструкции import.


В конце описания HostResolveImportedModule() говорится, что при импорте модулей, которым соответствует один и тот же путь, выполняется импорт одного и того же модуля:

import moduleA from 'path';
import moduleB from 'path';
import moduleC from 'path';

// moduleA, moduleB и moduleC -это одно и то же

moduleA === moduleB; // => true
moduleB === moduleC; // => true


Интересно, что спецификация указывает на то, что хост (браузер, среда Node.js, в общем — что угодно, пытающееся выполнить JavaScript-код) должен предоставлять конкретную реализацию HostResolveImportedModule().

Ответ


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

Вернёмся к нашему вопросу.

Модуль increment всегда выполняется лишь один раз:

// increment.js
let counter = 0;
counter++;

export default counter;


Вне зависимости от того, сколько раз был импортирован модуль increment, выражение counter++ вычисляется лишь один раз. Переменная counter, экспортируемая с использованием механизма экспорта по умолчанию, имеет значение 1.

Теперь взглянем на модуль consumer:

// consumer.js
import counter1 from './increment';
import counter2 from './increment';

counter1; // => 1
counter2; // => 1


В командах import counter1 from './increment' и import counter2 from './increment' используется один и тот же путь — './increment'. В результате оказывается, что импортируется один и тот же экземпляр модуля.

Получается, что в переменные counter1 и counter2 в consumer записано одно и то же значение 1.

Итоги


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

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

Если модуль импортируют несколько раз, но спецификатор модуля (путь к нему) при этом остаётся одним и тем же, то спецификация JavaScript гарантирует то, что импортирован будет один и тот же экземпляр модуля.

Уважаемые читатели! Как часто вы прибегаете к чтению спецификации JavaScript, выясняя особенности функционирования неких языковых конструкций?

1ba550d25e8846ce8805de564da6aa63.png

© Habrahabr.ru