Когда экспорт с именем ES6 асинхронно мутирует, почему изменения подхватываются импортерами?

#javascript #es6-modules

Вопрос:

Рассмотрим следующий пример:

 // module.mjs

export let member = "initial"

setTimeout(() => { 
  member = "mutated" 
}, 2000)
 
 // index.mjs

import { member } from "./module.mjs"

setInterval(() => { 
  console.log(member) 
}, 300);
 

Если вы запустите index.mjs его, он напечатает "initial" несколько раз и после 2-секундного тайм-аута начнет печать "mutated" .

Это меня удивляет. Я ожидал, что он просто продолжит печатать "initial" , так как ссылка на member изменилась. Я бы не удивился, если бы мой index.mjs выглядел так

 // index.mjs

import * as allExports from "./module.mjs"

setInterval(() => { 
  console.log(allExports.member) 
}, 300);
 

Ссылка на allExports не изменилась. Таким образом, получив доступ member в качестве свойства, я получаю доступ к новой ссылке. И вполне справедливо, что это приводит к тому же результату.

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

 // module.mjs

let member = "initial"

export default member

setTimeout(() => { 
  member = "mutated" 
}, 2000)
 
 // index.mjs

import member from "./module.mjs"

setInterval(() => { 
  console.log(member) 
}, 300);
 

index.mjs просто продолжает печатать "initial" в соответствии с моей интуицией. Хм, может быть, есть принципиальная разница между экспортом по умолчанию и экспортом по имени. Что, если я сделаю это:

 // module.mjs

export let member = "initial"

setTimeout(() => { 
  member = "mutated" 
}, 2000)
 
 // index.mjs

import * as allExports from "./module.mjs"

let { member } = allExports

setInterval(() => { 
  console.log(member) 
}, 300);
 

Вау, он продолжает печатать "initial" . Вот тут я действительно запутался. Я всегда предполагал, что

 import { member } from "./module.mjs"
 

это просто сокращение от импорта всего named-exports как объекта, а затем выполнения деструктурирования.

Поэтому мой вопрос таков: какой основополагающий принцип оправдывает такое уникальное поведение named-exports ?

Комментарии:

1. Шаг 1, когда основные принципы сбивают вас с толку: быстро прочтите официальную спецификацию, чтобы убедиться, что вы просто неправильно поняли механизм. Даже что-то вроде developer.mozilla.org/en-US/docs/web/javascript/reference/… сделает следующее: » Инструкция export используется при создании модулей JavaScript для экспорта живых привязок к функциям, объектам или примитивным значениям из модуля «.

2. (С Minusfour, объясняющим, почему allExports делает что-то другое со ссылкой на спецификацию)

Ответ №1:

Причина этого в том, что технически модули ES6 экспортируют и импортируют привязки, а не значения. Когда вы это сделаете:

 export let test = 0
 

Вам нужно думать, что вы test не экспортируете 0 . Это означает, что любое изменение переменной будет отражено на их импорте.

Причина, по которой это продолжает печататься initial :

 // index.mjs

import * as allExports from "./module.mjs"

let { member } = allExports

setInterval(() => { 
  console.log(member) 
}, 300);
 

Это потому, что allExports это особый объект. Вы создаете здесь новую привязку, и значение копируется, но вы теряете импортированную привязку.

Комментарии:

1. Интересный. Но почему это не относится к экспорту по умолчанию?

2. Об этом я ничего не знаю. Я предполагаю, что они хотели поддерживать уровень совместимости с другими модулями (например, модулями узлов), которые экспортируют не привязки, а единственное значение. У них есть местное название, хотя в соответствии со спецификацией.

3. И на самом деле, у вас может быть экспорт по умолчанию с привязкой. Если экспорт по умолчанию представляет собой объявление функции (с асинхронным, генератором или их комбинацией) или объявление класса, вы экспортируете привязку указанного объявления. Они также изменчивы, поэтому это работает.