[Перевод] Что происходит, когда 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
?
Для того чтобы ответить на этот вопрос, нужно понимать то, как 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, выясняя особенности функционирования неких языковых конструкций?